🤡博客主页:醉竺
🥰本文专栏:《高阶数据结构》
😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!
✨✨💜💛想要学习更多《高阶数据结构》点击专栏链接查看💛💜✨✨
目录
1. 顺序存储方式
2. 链式存储方式
3. 顺序存储的常用操作代码实现
3.1 树中每个节点的定义
3.2 二叉树的定义
3.2.1 二叉树各种接口的实现
3.2.2 二叉树顺序存储测试
4. 链式存储的常用操作代码实现
2.1 类定义、初始化和释放
2.2 创建树节点
2.3 二叉树常用操作
2.4 利用扩展二叉树的前序遍历序列来创建一棵二叉树
2.5 递归遍历二叉树
1. 前序遍历
2. 中序遍历
3. 后序遍历
4. 层序遍历
2.6 非递归遍历二叉树
1. 前序遍历(根左右)
2. 中序遍历(左根右)
3. 后序遍历(左右根)
2.7 通过遍历序列创建二叉树
1. 根据前序和中序遍历创建二叉树
2. 根据中序和后序遍历创建二叉树
5. 小结
如果对二叉树还不是了解的同学,可以先简单学习一下下面一篇文章,详细讲解了二叉树的相关概念和性质证明,以及遍历方法:
《二叉树的概念和性质》https://blog.csdn.net/weixin_43382136/article/details/136593735?spm=1001.2014.3001.5502《二叉树的深度优先遍历和广度优先遍历》https://blog.csdn.net/weixin_43382136/article/details/136658468?spm=1001.2014.3001.5502
学完本章,你将掌握:
- 二叉树顺序存储代码实现;
- 二叉树链式存储代码实现;
- 二叉树的前序遍历、中序遍历、后序遍历的递归实现和非递归实现;
- 通过前序和中序遍历序列创建二叉树;
- 通过中序和后序遍历序列创建二叉树;
- 二叉树的其他操作。
二叉树的存储一般有两种方式,一种是基于数组的 顺序 存储方式,一种是 链式 存储方式。它们有什么不同呢?
1. 顺序存储方式
顺序存储方式是用一段连续的内存单元(数组)依次从上到下、从左到右存储二叉树各个节点元素。
假设我们存储的是一棵完全二叉树,如果把根节点存储在数组中下标i = 1的位置,那么根据之前讲解的二叉树编号规律,左子节点就存储在下标2 * i = 2的位置,右子节点就存储在2 * i + 1 = 3的位置。这样就可以通过下标把整棵树串起来。
因为节点编号从1开始,所以数组中下标为0的位置可以空出来不用,让数组下标和节点编号保持一致。虽然这样浪费了一个存储空间,不过这点你可以自由决定。
参考下图1。
不难看到,数组的下标可以体现出节点的存储位置。换句话说,数组的下标能够体现出节点之间的逻辑关系(父子、兄弟)。
但如果存储的不是一棵完全二叉树,而是普通二叉树,那么存储的时候,也需要将其按照完全二叉树的方式来编号存储,这样肯定会浪费较多的数组存储空间。
参考下图2。
图2中虚线表示的节点表示不存在的节点,但在存储时却要给其留出位置。可以看到,下标6、8的数组空间都被浪费掉了。
你可以想一种极端的情况:如果有一棵深度为4的右斜树需要保存,会浪费多少个数组空间呢?
根据二叉树性质2,“高度为k的二叉树至多有个节点”,存储高度为 4 的二叉树,最坏情况下空间需求是,因为下标为0的空间不使用,所以最坏情况下空间需求是,而图3的右斜树恰恰需要个存储空间。即便下标为0的位置不计算在内,也整整浪费了11个位置。
可是,完全二叉树就不会存在这个问题。这就是完全二叉树最后一层叶节点都靠左侧排列这种硬性规定所带来的存储益处。换句话说, 顺序存储结构一般是用于存储完全二叉树。
2. 链式存储方式
链式存储方式要存储额外的左右子节点的指针,而顺序存储方式是不需要指针的,所以通常来讲, 链式存储方式多用于存储普通的二叉树。
链式存储中,二叉树的每个节点最多有两个子节点,所以为每个节点设计结构时一般都包括一个数据域和两个指针域,分别指向左孩子节点和右孩子节点。至于没有用到的指针,设置为nullptr即可。结构示意图如图4所示:
从图4可以看到,只要 拎住最上面的根节点,就可以通过左右子节点的指针把整棵树串起来。
利用这种节点结构所得到的二叉树存储结构,叫做 二叉链表,一个节点结构中带两个指针。有时候为了方便找到某个节点的父节点,还可以在设计节点结构的时候再增加一个指针,用于指向其父节点,这种节点结构所得到的二叉树存储结构,就叫做 三叉链表,也就是一个节点结构中带三个指针。
3. 顺序存储的常用操作代码实现
概念理清之后,我们就要开始代码层面的学习了。虽然顺序存储结构一般是用于存储完全二叉树,但为了让写出的代码也支持普通二叉树,所以我会用二叉树的顺序存储方式,存储图5左侧所示的普通二叉树。
下面是顺序存储代码的完整实现每一部分都有详细注释。
BinaryTree_Seq.h
3.1 树中每个节点的定义
#include <iostream>
using namespace std;
#define MaxSize 100 //数组的尺寸
enum ECCHILDSIGN //节点标记
{
E_Root, //树根
E_ChildLeft, //左孩子
E_ChildRight //右孩子
};
//树中每个节点的定义
template <typename T>//T代表数据元素的类型
struct BinaryTreeNode
{
T data; //数据域,存放数据元素
bool isValid;//该节点是否有效以应对非完全二叉树(只有保存了实际节点数据的节点才是有效的)
};
3.2 二叉树的定义
//二叉树的定义
template <typename T>
class BinaryTree
{
private:
BinaryTreeNode<T> SqBiTree[MaxSize + 1]; //存储二叉树节点的数组,为写程序方便,下标为[0]的数组元素不使用,因此这里+1
private:
bool ifValidRangeIdx(int index) //是否是一个有效的数组下标值
{
//位置必须合理
if (index < 1 || index> MaxSize) //[0]下标留着不用。因定义数组时定义的是MaxSize + 1,所以数组最大下标是MaxSize。
return false;
return true;
}
//前序遍历二叉树,其他的遍历方式在二叉树的链式存储中再详细书写代码和讲解
void preOrder(int index)
{
if (ifValidRangeIdx(index) == false) //位置不合理
return;
if (SqBiTree[index].isValid == false) //不是个合理的节点
return;
//根左右顺序
cout << (char)SqBiTree[index].data << ""; //输出节点的数据域的值,为方便观察,用char以显示字母
preOrder(2 * index); //递归方式前序遍历左子树
preOrder(2 * index + 1); //递归方式前序遍历右子树
}
public:
BinaryTree() //构造函数
{
for (int i = 0; i <= MaxSize; ++i) //注意数组的大小是MaxSize+1,所以这里i的终止值没问题
{
SqBiTree[i].isValid = false; //开始时节点无效,没保存任何数据
}
}
~BinaryTree() {}; //析构函数
public:
//创建一个树节点
int CreateNode(int parindex, ECCHILDSIGN pointSign, const T& e);
int getParentIdx(int sonindex);//获取父节点的下标
int getPointLevel(int index);//获取某个节点所在的高度:
int getLevel(); //获取二叉树的深度
bool ifCompleteBT();//判断是否是个完全二叉树
void PreOrderTraversal();//前序遍历二叉树,其他的遍历方式在二叉树的链式存储中再详细书写代码和讲解
};
3.2.1 二叉树各种接口的实现
BinaryTree_Seq.cpp
#include "BinaryTree_Seq.h"
创建一个树节点:
- 参数1:父节点所在数组下标;
- 参数2:标记所创建的是树根、左孩子、右孩子;
- 参数3:插入的树节点的元素值;
- 返回值:返回存储位置对应的数组下标,返回-1表示非法下标(执行失败)
//创建一个树节点
template <typename T>
int BinaryTree<T>::CreateNode(int parindex, ECCHILDSIGN pointSign, const T& e)
{
//非根节点,则一定是子节点,要求parindex一定要是个合理值
if (pointSign != E_Root)
{
if (ifValidRangeIdx(parindex) == false) //位置不合理
return -1;
if (SqBiTree[parindex].isValid == false) //父节点不可以无效
return -1;
}
int index = -1;
if (pointSign == E_Root) //根
{
index = 1;//根节点固定存储在下标为1的位置。
}
else if (pointSign == E_ChildLeft) //左孩子
{
//创建的是左孩子节点,节点i的左孩子节点的下标是2i
index = 2 * parindex;
if (ifValidRangeIdx(index) == false)
return -1; //非法下标
}
else //右孩子
{
//节点i的右孩子节点的下标是2i+1
index = 2 * parindex + 1;
if (ifValidRangeIdx(index) == false)
return -1; //非法下标
}
SqBiTree[index].data = e;
SqBiTree[index].isValid = true; //标记该下标中有有效数据
return index;
}
获取父节点的下标:
//获取父节点的下标
template <typename T>
int BinaryTree<T>::getParentIdx(int sonindex)
{
if (ifValidRangeIdx(sonindex) == false) //位置不合理
return -1;
if (SqBiTree[sonindex].isValid == false) //不是个合理的节点,不要尝试找父节点
return -1;
return int(sonindex / 2); //i的父节点是(i / 2)向下取整
}
获取某个节点所在的高度:
//获取某个节点所在的高度:
//根据二叉树性质五:具有n(n > 0)个节点的完全二叉树的高度为⌈log(n + 1)⌉或者⌊log(n)⌋ + 1。这里的对数都是以2为底
template <typename T>
int BinaryTree<T>::getPointLevel(int index)
{
if (ifValidRangeIdx(index) == false) //位置不合理
return -1;
if (SqBiTree[index].isValid == false)//不是个合理的节点,不要尝试找父节点
return -1;
//采用公式⌊log(n)⌋ + 1
int level = int(log(index) / log(2) + 1);//c++中的log(n)函数求的是以e(2.71828)为底的对数值,如果要求以数字m为底的对数值,则需要log(n)/log(m)
return level;
}
获取二叉树的深度:
//获取二叉树的深度
template <typename T>
int BinaryTree<T>::getLevel()
{
if (SqBiTree[1].isValid == false) //没根?
return 0;
int i;
for (i = MaxSize; i >= 1; --i)
{
if (SqBiTree[i].isValid == true) //找到最后一个有效节点
break;
} //end for
return getPointLevel(i);
}
判断是否是个完全二叉树:
//判断是否是个完全二叉树
template <typename T>
bool BinaryTree<T>::ifCompleteBT()
{
if (SqBiTree[1].isValid == false) //没根?这是二叉树吗?
return false;
int i;
for (i = MaxSize; i >= 1; --i)
{
if (SqBiTree[i].isValid == true) //找到最后一个有效节点
break;
} //end for
for (int k = 1; k <= i; ++k)
{
if (SqBiTree[k].isValid == false) //所有节点必须都要有效
return false;
}
return true;
}
前序遍历二叉树:
//前序遍历二叉树,其他的遍历方式在二叉树的链式存储中再详细书写代码和讲解
template <typename T>
void BinaryTree<T>::PreOrderTraversal()
{
if (SqBiTree[1].isValid == false) //没根?这是二叉树吗?
return;
preOrder(1); //根节点的数组下标是1,所以这里把根的下标传递过去
}
3.2.2 二叉树顺序存储测试
Test_BinaryTree_Seq.cpp
#include "BinaryTree_Seq.cpp"
int main()
{
BinaryTree<int> mytree;
//创建一棵二叉树
int indexRoot = mytree.CreateNode(-1, E_Root, 'A'); //创建树根节点A
int indexNodeB = mytree.CreateNode(indexRoot, E_ChildLeft, 'B'); //创建树根的左子节点B
int indexNodeC = mytree.CreateNode(indexRoot, E_ChildRight, 'C'); //创建树根的右子节点C
int indexNodeD = mytree.CreateNode(indexNodeB, E_ChildLeft, 'D'); //创建节点B的左子节点D
int indexNodeE = mytree.CreateNode(indexNodeC, E_ChildRight, 'E'); //创建节点C的右子节点E
int iParentIndexE = mytree.getParentIdx(indexNodeE); //获取某个节点的父节点的下标
cout << "节点E的父节点的下标是:" << iParentIndexE << endl;
int iLevel = mytree.getPointLevel(indexNodeD); //获取某个节点所在的高度
cout << "节点D所在的高度是:" << iLevel << endl;
iLevel = mytree.getPointLevel(indexNodeE);
cout << "节点E所在的高度是:" << iLevel << endl;
cout << "二叉树的深度是:" << mytree.getLevel() << endl;
cout << "二叉树是个完全二叉树吗?" << mytree.ifCompleteBT() << endl;
cout << "------------" << endl;
cout << "前序遍历序列为:";
mytree.PreOrderTraversal(); //前序遍历
return 0;
}
4. 链式存储的常用操作代码实现
下面我们熟悉一下二叉树链式存储的常用操作。
2.1 类定义、初始化和释放
#pragma once
#include <iostream>
#include <queue>
#include <stack>
using namespace std;
enum ECCHILDSIGN //节点标记
{
E_Root, //树根
E_ChildLeft, //左孩子
E_ChildRight //右孩子
};
//树中每个节点的定义
template <typename T> //T代表数据元素的类型
struct BinaryTreeNode
{
T data; //数据域,存放数据元素
BinaryTreeNode* leftChild, //左子节点指针
* rightChild; //右子节点指针
//BinaryTreeNode* parent; //父节点指针,可以根据需要决定是否引入
};
//二叉树的定义
template <typename T>
class BinaryTree
{
public:
BinaryTree(); // 构造函数
~BinaryTree(); // 析构函数
public:
//创建一个树节点
BinaryTreeNode<T>* CreateNode(BinaryTreeNode<T>* parentnode, ECCHILDSIGN pointSign, const T& e);
//释放树节点
void ReleaseNode(BinaryTreeNode<T>* pnode);
public:
int getSize(); //求二叉树节点个数
int getHeight(); //求二叉树高度(取左右子树中高度更高的)
//查找某元素节点
BinaryTreeNode<T>* SearchElem(const T& e);
//查找父节点
BinaryTreeNode<T>* GetParent(BinaryTreeNode<T>* tSonNode);
//树的拷贝
void CopyTree(BinaryTree<T>* targetTree);
void preOrder(); //前序遍历二叉树
void inOrder(); //中序遍历二叉树
void postOrder(); //后序遍历二叉树
void levelOrder();//层序遍历二叉树
void preOrder_noRecu(); //非递归方式前序遍历二叉树
void inOrder_noRecu(); //非递归方式中序遍历二叉树
void postOrder_noRecu(); //非递归后序遍历二叉树
//利用扩展二叉树的前序遍历序列来创建一棵二叉树
void CreateBTreeAccordPT(char* pstr);
//如何根据前序、中序遍历序列来创建一棵二叉树呢?
//pP_T:前序遍历序列,比如是“ABDCE”,pI_T:中序遍历序列,比如是“DBACE”
void CreateBTreeAccordPI(char* pP_T, char* pI_T);
//如何根据中序、后序遍历序列来创建一棵二叉树呢?
//pI_T:中序遍历序列,比如是“DBACE”,pPOST_T:后序遍历序列,比如是“DBECA”
void CreateBTreeAccordIPO(char* pI_T, char* pPOST_T);
private:
BinaryTreeNode<T>* root; //树根指针
};
//构造函数
template<class T>
BinaryTree<T>::BinaryTree()
{
root = nullptr;
}
//析构函数
template<class T>
BinaryTree<T>::~BinaryTree()
{
ReleaseNode(root);
};
//释放二叉树节点
template<class T>
void BinaryTree<T>::ReleaseNode(BinaryTreeNode<T>* pnode)
{
if (pnode != nullptr)
{
ReleaseNode(pnode->leftChild);
ReleaseNode(pnode->rightChild);
}
delete pnode;
}
2.2 创建树节点
//创建一个树节点
//参数1:父节点指针,参数2:标记所创建的是树根、左孩子、右孩子,参数3:插入的树节点的元素值
template<class T>
BinaryTreeNode<T>* BinaryTree<T>::CreateNode(BinaryTreeNode<T>* parentnode, ECCHILDSIGN pointSign, const T& e)
{
//将新节点创建出来
BinaryTreeNode<T>* tmpnode = new BinaryTreeNode<T>;
tmpnode->data = e;
tmpnode->leftChild = nullptr;
tmpnode->rightChild = nullptr;
//把新节点放入正确的位置
if (pointSign == E_Root)
{
//创建的是根节点
root = tmpnode;
}
if (pointSign == E_ChildLeft)
{
//创建的是左孩子节点
parentnode->leftChild = tmpnode;
}
else if (pointSign == E_ChildRight)
{
//创建的是右孩子节点
parentnode->rightChild = tmpnode;
}
return tmpnode;
}
在main主函数中,加入如下代码,创建一个如前面图5左侧所示的一棵二叉树。
BinaryTree<int> mytree;
//创建一棵二叉树
BinaryTreeNode<int>* rootpoint = mytree.CreateNode(nullptr, E_Root, 'A'); //创建树根节点A
BinaryTreeNode<int>* subpoint = mytree.CreateNode(rootpoint, E_ChildLeft, 'B'); //创建树根的左子节点B
subpoint = mytree.CreateNode(subpoint, E_ChildLeft, 'D'); //创建左子节点B下的左子节点D
subpoint = mytree.CreateNode(rootpoint, E_ChildRight, 'C'); //创建树根的右子节点C
subpoint = mytree.CreateNode(subpoint, E_ChildRight, 'E'); //创建右子节点C下的右子节点E
2.3 二叉树常用操作
接下来,我们将分别实现二叉树的一些常用操作,包括计算节点个数、求二叉树的高度、根据给的节点值查找某个节点、查找某个节点的父节点、二叉树的拷贝。
-
计算二叉树的节点个数
//求二叉树节点个数
template <typename T>
int BinaryTree<T>::getSize()
{
return _getSize(root);
}
template <typename T>
int _getSize(BinaryTreeNode<T>* tNode) //也可以用遍历二叉树的方式求节点个数
{
if (tNode == nullptr)
return 0;
return _getSize(tNode->leftChild) + _getSize(tNode->rightChild) + 1; //之所以+1,是因为还有个根节点
}
- 求二叉树高度(取左右子树中高度更高的)
//求二叉树高度(取左右子树中高度更高的)
template <typename T>
int BinaryTree<T>::getHeight()
{
return _getHeight(root);
}
template <typename T>
int _getHeight(BinaryTreeNode<T>* tNode)
{
if (tNode == nullptr)
return 0;
int lheight = _getHeight(tNode->leftChild); //左子树高度
int rheight = _getHeight(tNode->rightChild); //右子树高度
if (lheight > rheight)
return lheight + 1; //之所以+1,是因为还包括根节点的高度
return rheight + 1; //之所以+1,是因为还包括根节点的高度
}
-
查找某个节点(假设二叉树的节点各不相同)
// 查找某元素节点
template <typename T>
BinaryTreeNode<T>* BinaryTree<T>::SearchElem(const T& e)
{
return _SearchElem(root, e);
}
template <typename T>
BinaryTreeNode<T>* _SearchElem(BinaryTreeNode<T>* tNode, const T& e)
{
if (tNode == nullptr)
return nullptr;
if (tNode->data == e) //从根开始找
return tNode;
BinaryTreeNode<T>* p = _SearchElem(tNode->leftChild, e); //查找左子树
if (p != nullptr) //这里的判断不可缺少
return p;
return _SearchElem(tNode->rightChild, e); //左子树查不到,继续到右子树查找,这里可以直接return
}
-
查找某个节点的父节点
//查找某个节点的父节点
template <typename T>
BinaryTreeNode<T>* BinaryTree<T>::GetParent(BinaryTreeNode<T>* tSonNode)
{
return _GetParent(root, tSonNode);
}
template <typename T>
BinaryTreeNode<T>* _GetParent(BinaryTreeNode<T>* tParNode, BinaryTreeNode<T>* tSonNode)
{
if (tParNode == nullptr || tSonNode == nullptr)
return nullptr;
if (tParNode->leftChild == tSonNode || tParNode->rightChild == tSonNode)
return tParNode;
BinaryTreeNode<T>* pl = _GetParent(tParNode->leftChild, tSonNode);
if (pl != nullptr)
return pl;
return _GetParent(tParNode->rightChild, tSonNode);
}
-
树的拷贝
CopyTree 函数将一个二叉树(源树)的内容拷贝到另一个二叉树(目标树)中
//树的拷贝
template <typename T>
void BinaryTree<T>::CopyTree(BinaryTree<T>* targetTree)
{
_CopyTree(root, targetTree->root);
}
template <typename T>
void _CopyTree(BinaryTreeNode<T>* tSource, BinaryTreeNode<T>*& tTarget) //注意第二个参数类型为引用
{
if (tSource == nullptr)
{
tTarget = nullptr;
}
else
{
tTarget = new BinaryTreeNode<T>;
tTarget->data = tSource->data;
_CopyTree(tSource->leftChild, tTarget->leftChild); //对左子树进行拷贝
_CopyTree(tSource->rightChild, tTarget->rightChild);//对右子树进行拷贝
}
}
2.4 利用扩展二叉树的前序遍历序列来创建一棵二叉树
在前面讲解扩展二叉树时《叉树的深度优先遍历和广度优先遍历》,你已经知道,给出一个扩展二叉树的前序遍历序列,能够唯一确定一棵二叉树。前面图5中右侧的扩展二叉树,其前序遍历序列是“ABD###C#E##”,那么如果想通过这个遍历序列把这棵二叉树创建,是否可以做到呢?可以。
//利用扩展二叉树的前序遍历序列来创建一棵二叉树
template<class T>
void BinaryTree<T>::CreateBTreeAccordPT(char* pstr)
{
_CreateBTreeAccordPT(root, pstr);
}
//利用扩展二叉树的前序遍历序列创建二叉树的递归函数
template<class T>
void _CreateBTreeAccordPT(BinaryTreeNode<T>*& tnode, char*& pstr)//参数为引用类型,确保递归调用中对参数的改变会影响到调用者
{
if (*pstr == '#')
{
tnode = nullptr;
}
else
{
tnode = new BinaryTreeNode<T>; //创建根节点
tnode->data = *pstr;
_CreateBTreeAccordPT(tnode->leftChild, ++pstr); //创建左子树
_CreateBTreeAccordPT(tnode->rightChild, ++pstr);//创建右子树
}
}
在main主函数中,注释掉以往的代码,新增如下测试代码。
BinaryTree<int> mytree2;
mytree2.CreateBTreeAccordPT((char *)"ABD###C#E##");
通过跟踪调试,不难看到,上述的mytree2所存储的数据正是图5左侧所示的二叉树。
2.5 递归遍历二叉树
二叉树创建出来后,就可以通过前面讲解过的遍历方式,将树中各个节点的内容输出出来以方便观察。我们从深度、广度优先遍历两种方式的角度去了解。
-
1. 前序遍历
//前序遍历二叉树
template <typename T>
void BinaryTree<T>::preOrder()
{
_preOrder(root);
}
template <typename T>
void _preOrder(BinaryTreeNode<T>* tNode)
{
if (tNode != nullptr) //若二叉树非空
{
//根左右顺序
cout << (char)tNode->data << " "; //输出节点的数据域的值,为方便观察,用char以显示字母
_preOrder(tNode->leftChild); //递归方式前序遍历左子树
_preOrder(tNode->rightChild); //递归方式前序遍历右子树
}
}
-
2. 中序遍历
//中序遍历二叉树
template <typename T>
void BinaryTree<T>::inOrder()
{
_inOrder(root);
}
template <typename T>
void _inOrder(BinaryTreeNode<T>* tNode) //中序遍历二叉树
{
if (tNode != nullptr) //若二叉树非空
{
//左根右顺序
_inOrder(tNode->leftChild); //递归方式中序遍历左子树
cout << (char)tNode->data << " "; //输出节点的数据域的值
_inOrder(tNode->rightChild); //递归方式中序遍历右子树
}
}
-
3. 后序遍历
//后序遍历二叉树
template <typename T>
void BinaryTree<T>::postOrder()
{
_postOrder(root);
}
template <typename T>
void _postOrder(BinaryTreeNode<T>* tNode) //后序遍历二叉树
{
if (tNode != nullptr) //若二叉树非空
{
//左右根顺序
_postOrder(tNode->leftChild); //递归方式后序遍历左子树
_postOrder(tNode->rightChild); //递归方式后序遍历右子树
cout << (char)tNode->data << " "; //输出节点的数据域的值
}
}
在main主函数中,继续增加如下代码对刚刚创建的树进行前序、中序和后序遍历。
cout << "前序遍历序列为:";
mytree2.preOrder(); //前序遍历
cout << endl; //换行
cout << "中序遍历序列为:";
mytree2.inOrder(); //中序遍历
cout << endl; //换行
cout << "后序遍历序列为:";
mytree2.postOrder(); //后序遍历
新增代码的执行结果如下,正是对图5左侧所示的二叉树进行前序、中序、后序遍历得到的结果。
-
4. 层序遍历
除了对二叉树进行前序、中序、后序遍历之外,还有一种遍历方式叫层序遍历。前面说过,二叉树的层序遍历需要借助队列来完成,因为难以估计二叉树节点个数,所以使用顺序队列不合适,这里就用链式队列。
//层序遍历二叉树
template <typename T>
void BinaryTree<T>::levelOrder()
{
_levelOrder(root);
}
template <typename T>
void _levelOrder(BinaryTreeNode<T>* tNode)
{
if (tNode != nullptr) // 若二叉树非空
{
BinaryTreeNode<T>* tmpnode;
queue<BinaryTreeNode<T>*> q; // 使用STL中的queue容器
q.push(tNode); // 先把根节点指针入队
while (!q.empty()) // 循环判断队列是否为空
{
tmpnode = q.front(); // 获取队列前端的元素
q.pop(); // 出队列
cout << (char)tmpnode->data << " ";
if (tmpnode->leftChild != nullptr)
q.push(tmpnode->leftChild); // 左孩子入队
if (tmpnode->rightChild != nullptr) // 右孩子入队
q.push(tmpnode->rightChild);
} // end while
}
}
在main主函数中,继续增加如下代码对刚刚创建的树进行层序遍历。
cout << endl; //换行
cout << "层序遍历序列为:";
mytree2.levelOrder();
新增代码的执行结果如下。
这里可以进一步思考一下,如何在上述层序遍历代码中加入一些代码来 判断二叉树是否是一棵完全二叉树 呢?
如果某个节点的左子节点不存在而右子节点存在,那么这棵二叉树就不是完全二叉树,所以上述层序遍历代码中,在出队列一个节点后,可以加入如下代码来判断一棵二叉树是否是完全二叉树。
if(tmpnode->leftChild == nullptr && tmpnode->rightChild != nullptr)
{
//这棵二叉树不是一棵完全二叉树。
}
上述无论何种遍历方式,每个节点被访问的次数都是有限的,所以遍历操作的时间复杂度和二叉树的节点个数成正比,即二叉树遍历的时间复杂度为O(n)。
2.6 非递归遍历二叉树
在前面的二叉树遍历代码实现中,除层序遍历外,前序、中序、后序遍历采用的都是递归方式来实现的,这种实现方法代码比较简单。但实际的面试中,有时也会要求采用非递归的方式实现前序、中序、后序遍历,这往往需要借助栈来实现。并且实际的应用中,如果二叉树的高度特别高,节点特别多,用递归的方式遍历可能会导致栈溢出,此时就需要采用非递归方式遍历了。
-
1. 前序遍历(根左右)
你可以根据图片提示,自己手工绘制一下入栈和出栈的过程,这样也会对通过栈实现前序遍历有一个更深刻的认识。
//非递归方式前序遍历二叉树
template <typename T>
void BinaryTree<T>::preOrder_noRecu()
{
_preOrder_noRecu(root);
}
template <typename T>
void _preOrder_noRecu(BinaryTreeNode<T>* tRoot)
{
if (tRoot == nullptr)
return;
stack<BinaryTreeNode<T>*> s;
s.push(tRoot); // 根节点入栈
BinaryTreeNode<T>* tmpnode;
while (!s.empty()) // 栈不空
{
tmpnode = s.top(); // 获取栈顶元素
s.pop(); // 栈顶元素出栈
cout << (char)tmpnode->data << " "; // 访问栈顶元素
if (tmpnode->rightChild != nullptr) // 注意先判断右树再判断左树
{
s.push(tmpnode->rightChild); // 把右树入栈
}
if (tmpnode->leftChild != nullptr)
{
s.push(tmpnode->leftChild); // 把左树入栈
}
} // end while
}
增加下列测试代码。
cout << "非递归方式前序遍历序列为:";
mytree2.preOrder_noRecu();
cout << endl;
-
2. 中序遍历(左根右)
相应的代码我也放在了下面,不过写法不限于这一种,有兴趣可以发挥自己的想象力,根据“左根右”这个遍历顺序写出不同的非递归遍历代码。
//非递归方式中序遍历二叉树
template <typename T>
void BinaryTree<T>::inOrder_noRecu()
{
_inOrder_noRecu(root);
}
template <typename T>
void _inOrder_noRecu(BinaryTreeNode<T>* tRoot)
{
if (tRoot == nullptr)
return;
stack<BinaryTreeNode<T>*> s;
BinaryTreeNode<T>* tmpnode = tRoot;
while (tmpnode != nullptr || !s.empty()) {
while (tmpnode != nullptr) {
s.push(tmpnode); // 将“当前节点的左节点”入栈
tmpnode = tmpnode->leftChild; // 将“当前节点的左节点”重新标记为当前节点。
} //end while
tmpnode = s.top(); // 对栈顶元素出栈(同时获取了栈顶元素)
s.pop();
cout << (char)tmpnode->data << " ";
// 查看右树
tmpnode = tmpnode->rightChild; // 将刚刚从栈顶出栈的元素的右节点标记为当前节点
}//end while
}
增加下列测试代码。
cout << "非递归方式中序遍历序列为:";
mytree2.inOrder_noRecu();
cout << endl;
新增代码的执行结果如下。
-
3. 后序遍历(左右根)
-
初始化:
- 使用两个栈 s1 和 s2。
- 将根节点压入 s1 栈。
-
遍历过程:
- 从 s1 栈中弹出一个节点 node。
- 将 node 压入 s2 栈。
- 如果 node 有左子节点,将左子节点压入 s1 栈。
- 如果 node 有右子节点,将右子节点压入 s1 栈。
-
输出结果:
- 当 s1 栈为空时,s2 栈中存储的节点顺序是根节点在最底部,左子节点在右子节点之上,这正是后序遍历的逆序。
- 依次从 s2 栈中弹出节点并输出其值,即可得到后序遍历的结果。
//非递归后序遍历二叉树
template <typename T>
void BinaryTree<T>::postOrder_noRecu()
{
_postOrder_noRecu(root);
}
template <typename T>
void _postOrder_noRecu(BinaryTreeNode<T>* tRoot)
{
if (tRoot == NULL) return;
stack<BinaryTreeNode<T>*> s1, s2;
s1.push(tRoot);
while (!s1.empty()) {
BinaryTreeNode<T>* node = s1.top();
s1.pop();
s2.push(node);
if (node->leftChild) {
s1.push(node->leftChild);
}
if (node->rightChild) {
s1.push(node->rightChild);
}
}
while (!s2.empty()) {
BinaryTreeNode<T>* node = s2.top();
s2.pop();
std::cout << (char)node->data << " ";
}
}
增加下列测试代码。
cout << "非递归方式后序遍历序列为:";
mytree2.postOrder_noRecu();
cout << endl;
新增代码的执行结果如下。
2.7 通过遍历序列创建二叉树
前面我们说过一个结论:
-
1. 根据前序和中序遍历创建二叉树
//如何根据前序、中序遍历序列来创建一棵二叉树呢?
//pre:前序遍历序列,比如是“ABDCE”,mid:中序遍历序列,比如是“DBACE”
template <typename T>
void BinaryTree<T>::CreateBTreeAccordPI(char* pre, char* mid)
{
_CreateBTreeAccordPI(root, pre, mid, strlen(pre));
}
template <typename T>
void _CreateBTreeAccordPI(BinaryTreeNode<T>*& tnode, char* pre, char* mid, int n)//参数1为引用类型,确保递归调用中对参数的改变会影响到调用者,n:节点个数
{
if (n == 0)
{
tnode = nullptr;
}
else
{
//(1)在中序遍历序列中找根,前序遍历序列中根在最前面
int index = 0; //下标
while (pre[0] != mid[index])
++index;
tnode = new BinaryTreeNode<T>; //创建根节点
tnode->data = mid[index]; //第一次index=2
//(2)创建左孩子
_CreateBTreeAccordPI(
tnode->leftChild, //创建左孩子
pre + 1, //找到前序遍历序列中左树开始节点的位置,这里跳过第一个(根)节点A,得到的是“BDCE”
mid, //不需要改动,仍旧是“DBACE”
index //左孩子有2个节点
);
//(3)创建右孩子
_CreateBTreeAccordPI(
tnode->rightChild, //创建右孩子
pre + index + 1, //找到前序遍历系列中右树开始节点的位置,不难发现,前序遍历序列和中序遍历序列右树开始节点的位置相同,得到的是“CE”
mid + index + 1, //找到中序遍历系列中右树开始节点的位置。得到的是“CE”
n - index - 1 //右孩子节点数
);
}
}
- 首先,前序序列的第一个字符 pre[0] 为根,然后在中序序列中查找根所在的位置,用index记录查找长度,找到后以根为中心,划分出左右子树。
- 左子树:前序序列中的首地址为 pre+1, 中序序列的首地址为 mid,长度为 index。
- 右子树:前序序列中的首地址为 pre+index+1, 中序序列的首地址为 mid+index+1,长度为 len-index-1。右子树的长度为总长度减去左子树的长度,再减去根。确定参数后,再递归求解左右子树即可。
第一次树根及左右子树划分如下图所示:
-
2. 根据中序和后序遍历创建二叉树
//如何根据中序、后序遍历序列来创建一棵二叉树呢?
//mid:中序遍历序列,比如是“DBACE”,post:后序遍历序列,比如是“DBECA”
template <typename T>
void BinaryTree<T>::CreateBTreeAccordIPO(char* mid, char* post)
{
_CreateBTreeAccordIPO(root, mid, post, strlen(mid));
}
template <typename T>
void _CreateBTreeAccordIPO(BinaryTreeNode<T>*& tnode, char* mid, char* post, int n)//参数1为引用类型,确保递归调用中对参数的改变会影响到调用者,n:节点个数
{
//可以通过后序遍历找到根节点
if (n == 0)
{
tnode = nullptr;
}
else
{
//(1)在中序遍历序列中找根,后序遍历序列中根在最后面
int index = 0; //下标
while (post[n - 1] != mid[index])
++index;
tnode = new BinaryTreeNode<T>; //创建根节点
tnode->data = mid[index]; //第一次index=2
//(2)创建左孩子
_CreateBTreeAccordIPO(
tnode->leftChild, //创建左孩子
mid, //不需要改动,仍旧是“DBACE”,因为开头的都是左孩子
post, //不需要改动,仍旧是“DBECA”,因为开头的都是左孩子
index //左孩子节点数
);
//(3)创建右孩子
_CreateBTreeAccordIPO(
tnode->rightChild, //创建右孩子
mid + index + 1, //找到中序遍历系列中右树开始节点的位置。得到的是“CE”
post + index, //找到后序遍历系列中右树开始节点的位置。得到的是“ECA”
n - index - 1 //右孩子节点数
);
}
}
测试用例main.cpp:
#include "BinaryTree_Link.cpp"
int main()
{
BinaryTree<int> mytree;//创建一棵二叉树
BinaryTreeNode<int>* rootpoint = mytree.CreateNode(nullptr, E_Root, 'A'); //创建树根节点A
BinaryTreeNode<int>* subpoint = mytree.CreateNode(rootpoint, E_ChildLeft, 'B'); //创建树根的左子节点B
subpoint = mytree.CreateNode(subpoint, E_ChildLeft, 'D'); //创建左子节点B下的左子节点D
subpoint = mytree.CreateNode(rootpoint, E_ChildRight, 'C'); //创建树根的右子节点C
subpoint = mytree.CreateNode(subpoint, E_ChildRight, 'E'); //创建右子节点C下的右子节点E
BinaryTree<int> mytree2;
//mytree2.CreateBTreeAccordPT((char*)"ABD###C#E##"); //根据扩展二叉树前序
//mytree2.CreateBTreeAccordPI((char*)"ABDCE", (char*)"DBACE"); //根据前序+中序
mytree2.CreateBTreeAccordIPO((char*)"DBACE", (char*)"DBECA"); //根据中序+后序
//-----------------
cout << "前序遍历序列为:";
mytree2.preOrder(); //前序遍历
cout << endl; //换行
cout << "中序遍历序列为:";
mytree2.inOrder(); //中序遍历
cout << endl; //换行
cout << "后序遍历序列为:";
mytree2.postOrder(); //后序遍历
//----------------
cout << endl; //换行
cout << "层序遍历序列为:";
mytree2.levelOrder();
//---------------
cout << endl;
cout << "二叉树节点个数为:" << mytree2.getSize() << endl;
//---------------
cout << "二叉树的高度为:" << mytree2.getHeight() << endl;
//---------------
//查找某个节点
int val = 'B';
BinaryTreeNode<int>* p = mytree2.SearchElem(val);
if (p != nullptr)
cout << "找到了值为" << (char)val << "的节点" << endl;
else
cout << "没找到值为" << (char)val << "的节点" << endl;
//---------------
//查找某个节点的父节点
BinaryTreeNode<int>* parp = mytree2.GetParent(p);
if (parp != nullptr)
cout << "找到了值为" << (char)val << "的节点的父节点" << (char)parp->data << endl;
else
cout << "没找到值为" << (char)val << "的节点的父节点" << endl;
//--------------
//测试树的拷贝
BinaryTree<int> mytree3;
mytree2.CopyTree(&mytree3);
//--------------
cout << "非递归方式前序遍历序列为:";
mytree2.preOrder_noRecu();
cout << endl;
//---------------
cout << "非递归方式中序遍历序列为:";
mytree2.inOrder_noRecu();
cout << endl;
//---------------
cout << "非递归方式后序遍历序列为:";
mytree2.postOrder_noRecu();
cout << endl;
//---------------
return 0;
}
5. 小结
这节课,我们学习了二叉树的两种存储方式——顺序存储方式、链式存储方式,以及他们的实现方式。
二叉树的顺序存储,是用一段连续的内存依次从上到下从左到右存储二叉树各个节点元素,但是很容易造成存储空间的浪费,因此顺序存储往往比较适合存储完全二叉树。
二叉树的链式存储,需要存储额外的左右节点指针。虽然增加了存储的灵活性,但也消耗了更多的存储空间。链式存储方式中,还涉及到了二叉链表和三叉链表的概念。二叉链表的一个节点结构带有两根指针以指向左右孩子节点,而三叉链表中一个节点结构带有三根指针,其中第三个指针用于指向当前节点的父节点,这给找当前节点的父节点带来了最大的便利。
由于与顺序存储相比,二叉树链式存储更加常用,所以这节课我将重点放在了二叉树链式存储的常用操作。给出了创建树节点,获取二叉树的前序、中序、后序、层序遍历序列,获取二叉树节点个数,获取二叉树高度,查找某个节点,查找某个节点的父节点,树的拷贝等操作代码。这里要重点记忆二叉树前序、中序、后序遍历代码,理解性记忆层序遍历代码,以防在面试中出现。
此外,我们也利用非递归的编程方式实现了二叉树的前序、中序、后序遍历。注意, 非递归的遍历操作需要借助栈来实现。这些代码需要我们简单理解、适当记忆,万一面试中出现,能说上几句就是好的。
最后,如何从无到有创建出一棵二叉树来呢?我们在最后用前序、中序遍历序列创建了一棵二叉树,这些代码都不复杂,但一定可以作为参考和借鉴,帮助你进一步拓展创建二叉树的思路。
需要源代码的可以点击我的gittee:数据结构与算法C++版