文章目录
树的表示
typedef struct TreeNode * BinTree;
struct TreeNode
{
int Data; // Stored value
BinTree Left; // 左二子Node
BinTree Right; // 右二子Node
};
一、二叉树的遍历
核心问题: 二维结构的线性化。
根据访问节点本使用的存储结构不同,分为栈(递归,递归本质就是栈的应用),队列遍历。
基于栈遍历
根据访问节点的时机不同分为三种遍历方式:
- 先序遍历:先序遍历是第一次"遇到"该结点时访问。
- 中序遍历:中序遍历是第二次"遇到"该结点(此时该结点从左子树返回)时访问
- 后序遍历:后序遍历是第三次"遇到"该结点(此时该结点从右子树返回)时访问
1、先序遍历
递归过程
- 访问根节点
- 先序遍历二叉树的左子树
- 先序遍历二叉树的右子树
1.1、先序递归遍历算法
void PreOrderTraversal(BinTree BT)
{
if (BT)
{
pritntf("%5d", BT-> Data); // 打印根节点
PreOrderTraversal(BT-> Left); // 进入左子树
PreOrderTraversal(BT-> Right); // 进入右子树
}
}
1.2、先序非递归遍历算法
void PreOrderTraversal (BinTree BT)
{
BinTree T = BT;
Stack S = CreateStack(MaxSize); // 创建并初始化堆栈S
while(T || !IsEmpty(S))
{
while(T)
{ // 一直向左并将沿途节点压入堆栈当中
Push(S,T);
Printf("5d",T-> Data);
T = T->Left; // 遍历左子树
}
if(!IsEmpty(S))
{
T = Pop(s);
T = T->Right; // 访问右结点
}
}
}
2、中序遍历
递归过程:
- 中序遍历二叉树左子树
- 访问根节点
- 中序遍历二叉树右子树
2.1、中序递归遍历算法
void InOrderTraversal(BinTree BT)
{
if(BT)
{
InOrderTraversal(BT->Left);
Printf("%5d", BT-> Data);
InOrderTraversal(BT->Right);
}
}
2.2、中序非递归遍历算法
void InOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = CreateStack(MaxSize);
while( T || !IsEmpty(S))
{
while(T)
{
Push(S,T);
T = T-> Left;
}
if(!IsEmpty(S))
{
T = Pop(S);
Printf("5d", T-> Data);
T = T-> Right;
}
}
}
3、后序遍历
递归过程:
- 后序遍历二叉树的左子树
- 后序遍历二叉树的右子树
- 访问根节点
3.1、后序递归遍历算法
void PostOrderTraversal(BinTree BT)
{
if(BT)
{
PostOrderTraversal(BT-> Left);
PostOrderTraversal(BT-> Right);
Printf("%5d", T->Data);
}
}
3.2、后序非递归遍历算法
思考:后序遍历是第三次访问节点时,将节点数据打印。而上述前序中序均是访问1、2次,代码也是最多访问2次,因此不再适合后序遍历,需要将出栈的节点再次压栈。
方法一:
思考:记录每次节点的访问次数,访问到第三次就输出节点数据。
void PostOrderTraversal(BinTree BT)
{
BinTree T = BT;
Stack S = CreateStack(MaxSize);
while(T || !IsEmpty(S))
{
T->times = 1; // 给树节点带一个出现次数空间,并首先置为1
Push(S,T);
T = T-> Left;
if(!IsEmpty(S))
{
T = Pop(S); // 第二、三次遇到此节点
if(T->times == 1) // 第二次遇到直接压入栈
{
Push(S,T);
++ T->Times;
T = T->Right;
}
elseif( T->Times == 2)
{
Print("%5d", T-> Data); // 第三次遇到打印
T = NULL;
}
}
}
}
评价: 上述方式有一个明显缺点,就是占用空间多,树节点中还包括被访问次数。而且对于没有右儿子的节点无需再次压栈,运行时间也会变长。
方法一对应的树节点应该被从新定义:
typedef struct BinTree
{
int Data; // 节点数据
struct BinTree *Left; // 左孩子指针
struct BinTree *Right; // 右孩子指针
int times; // 被访问次数
}BinTree, *BinTree;
方法二: (出自慕课大佬的评论)
思考:利用判断条件,判断出栈的节点是否需要再次被压栈。
void PostOrderTraversal(BinTree BT)
{
BinTree T = BT;
BinTree temp = NULL; // 保存上次出栈的节点,防止出栈之后无法访问
Stack S = CreateStack(MaxSize); // 创建栈
while(T || !IsEmpty(S))
{
while(T)
{
Push(S,T);
T = T-> Left;
}
if(!IsEmpty(S))
{
T = Pos(S);
// 若出栈的节点没有右儿子 或者 有右二子同时此节点上次出栈的节点就是自己右二子,则可以出栈。
if(T-> Right == NULL || T-> Right == temp)
{
Printf("%5d", T-> Data);
tempt = T; // 跟新上次保存此次出栈节点
T = NULL; // 表示子树遍历玩,指针置空,后续将弹出其父节点
}
// 出栈节点有右节点同时上次出栈的并非其右节点,则需要再次压栈,方便访问其右节点数据
else
{
Push(S,T); // 节点再次压栈
T = T-> Right; // 访问右节点
}
}
}
}
基于队列遍历
层序遍历
遍历过程:从上至下,从左至右访问所有节点。
本质: 先根节点入队,然后:
- 从队列中取出一个元素;
- 访问该元素所指节点
- 若该元素所指节点的左、右儿子节点非空,则将其做左、右儿子的指针顺序入队 。
void LevelOrderTraversal(BinTree BT)
{
Queue Q; BinTree T;
if(!BT) {return;} // 若是空树则直接返回
Q = CreateQueue(MaxSize); // 创建并初始化队列Q
AddQ(Q, BT); // 根节点入队
while(!IsEmpty(Q))
{
T = DeleteQ(Q);
Printf("%d\n", T-> Data); // 访问出栈队列节点数据
// 若出队节点有左右儿子,按顺序将节点左右儿子入队
if(T-> Left) { AddQ(Q, T-> Left;)}
elseif(T-> Right){ AddQ(Q, T-> Right;)}
}
}
二、例题以及应用
例题1
例题1:遍历二叉树的应用:输出二叉树的叶子节点。
思考:只要在遍历节点的时候判断 “ 左右子树是否为空 ” 。
void PerOrderPrintLeaves(BinTree BT)
{
if(BT)
{
if(!BT-> Left && BT-> Right)
{
printf("%d", BT-> Data);
}
PerOrderPrintLeaves(BT-> Left);
PerOrderPrintLeaves(BT-> Right);
}
}
例题二
例题二:求二叉树的高度
思考:从最底层出发。
- [1] 当只有0层时,也就是空树。返回0,表示空二叉树高度为0。
- [2] 当只有1层时。需要返回1,使用递归编程,空树作为基线条件。那么返回树的高度在基线返回值上加1。
- [3 ]根据上述推导,因此树的高度就是左右子树最高者加1。
int PostOrderGetHegint(BinTree BT)
{
if(BT)
{
int HL, HR, MaxH;
HL = PostOrderGetHegint(BT-> Left);
HR = PostOrderGetHegint(BT-> Right);
MaxH = (HL >= HR) ? HL : HR;
return (MaxH + 1);
}
else
{
return 0;
}
}
例题三
例题三: 由两种遍历序列确定二叉树。(但是必须有中序遍历)
后序序列: FDEBGCA 后序序列: FDBEACG 后序序列?
思考:
- [1] 根据 后序遍历序列 最后一个节点 确定根节点。 如题:A为根节点
- [2] 根据根节点在 中序序列中 分割左右两个子序列。
- [3] 对左右子树重复上述操作。
答案:ABDFECG。