目录
二叉树的遍历
以上为自己理解,如有错误,欢迎指正,互相学习,共同进步!
二叉树的遍历
一、二叉树的遍历
二叉树的遍历是按照某种规律对二叉树中的每个结点进行访问且只访问一次的过程。
二叉树的基本结构是由根结点、左子树、右子树三部分组成。
typedef struct Node { DataType data; struct Node*Lchild; struct Node*Rchild; }BiTNode,*BiTree;
遍历二叉树有三种策略:
先上后下、先左后右、先右后左。
本次重点讨论第二种、先左后右的遍历。
如图:先左后右的深度遍历二叉树时的局部搜索路线:
遍历操作要求是每个结点只访问一次,如图,每个结点先后都要遍历三次,而知访问一次,有三种对根结点的访问方式:
1、先序遍历:先访问根结点、遍历左子树、遍历右子树
2、中序遍历:遍历左子树、访问根节点、遍历右子树
3、后序遍历:遍历左子树、遍历右子树、访问根结点
二、二叉树遍历的递归
先序、中序、后序遍历都是按照递归定义、在各子树的遍历中,也应该按照遍历次序规律对子树的各结点进行遍历。
1、两个例子便于理解三种遍历方式:
图所示的二叉树、先序、中序、后序遍历的结点序列如下:
先序遍历:A、B、D、G、C、E、F、H
中序遍历:B、G、D、A、E、C、H、F
后序遍历:G、D、B、E、H、F、C、A
只对图一解释为什么是这样的结果序列:
遍历时需要清楚每个结点的左子树、右子树、和其子树的左子树、右子树。
先序遍历:
访问根结点A、遍历左子树,遍历右子树,遍历时每个子树也要按照先序的方式。
访问根结点A。
遍历左子树B——访问其根结点B、遍历左子树(没有左子树)、遍历右子树D {——先访问其根结点D、遍历其左子树G、遍历其右子树(没有右子树)}。遍历左子树结点序列为:
B、D、G。
遍历右子树C——访问其根结点C、遍历其左子树E、遍历其右子树F{——先访问其根结点F、遍历左子树H、遍历右子树(没有右子树)}。遍历右子树结点序列为:C、E、F、H。
遍历结束!A、B、D、G、C、E、F、H。
中序遍历:
先遍历左子树、访问根结点、遍历右子树。
遍历其左子树B——先遍历B的左子树,因为没有左子树,然后访问其根结点B、遍历其右子树D(——先遍历其左子树G、然后访问根结点D、遍历右子树,因为没有右子树)。左子树遍历结点序列为:B、G、D。
访问根结点A。
遍历其右子树C——先遍历其左子树E、访问其根结点C、遍历其右子树F(——先遍历其左子树H、然后访问其根结点F、其没有右子树)。右子树遍历结点序列为:E、C、H、F。
遍历结束!B、G、D、A、E、C、H、F。
后序遍历:
遍历左子树、遍历右子树、访问根结点。
遍历左子树B——先遍历其左子树,没有左子树,遍历其右子树D(——遍历其左子树G、遍历其右子树、没有右子树、访问其根结点D),然后访问根结点B。左子树遍历结点序列为:G、D、B。
遍历右子树C——先遍历其左子树E、遍历其右子树F(——遍历其左子树H、没有右子树,访问根结点F)、访问根结点C。右子树遍历结点序列为:E、H、F、C。
访问根结点A。
遍历结束!G、D、B 、E、H、F、C、A。
是不是理解了,第二个例子自己练一练吧!
先序遍历:A、B、D、G、C、E、H、F
中序遍历:D、G、B、A、E、H、C、F
后序遍历:G、D、B、H、E、F、C、A
2、二叉树遍历的递归实现
先序遍历二叉树的算法:
void PreOrder(BiTree root)
{ //root为指向根结点的指针
if(root)
{
Visit(root->data);
PreOrder(root->LChild);
PreOrder(root->RChild);
}
}
中序遍历二叉树的算法:
void InOrder(BiTree root)
{ //root为指向根结点的指针
if(root)
{
PreOrder(root->LChild);
Visit(root->data);
PreOrder(root->RChild);
}
}
后序遍历二叉树的算法:
void PostOrder(BiTree root)
{ //root为指向根结点的指针
if(root)
{
PreOrder(root->LChild);
PreOrder(root->RChild);
Visit(root->data);
}
}
三、二叉树遍历的非递归
1、二叉树遍历的非递归
因为递归算法执行耗费时间、空间资源较多,需要设计非递归的遍历算法
如何进行设计呢?
大多数递归问题的非递归设计可以用栈消除递归。
原因:
栈既是一个存容器,也是一个控制结构。对于递归算法的调用和退出,栈提供先进后出的控制结构,调用时,栈可以保存必要信息,退出时,可以从栈取出相关信息。
2、三种遍历方式的非递归实现的异同
三种遍历方式的相同点:
遍历放式只是三次经过结点时哪一次访问结点,但无论哪次经过时访问结点,第一次经过结点时,必须保留该结点信息,以备下次经过时使用结点。否则,从左子树返回到右子树,因找不到结点信息,无法进行该节点和右子树的访问与遍历。
因此进入左子树前,必须用栈保存结点信息。
不同之处:
访问左子树后:
对于中序遍历,需要访问结点,然后进入右子树,所以需要从栈中取的结点的信息,便于通过右孩子指针进入右子树。
访问右子树后:
对于中序遍历和先序遍历而言,访问右子树结束后,该结点没有要处理的工作,可直接退到二叉树的上一层(下面会解释什么意思),不必继续保留在栈中。
而对于后序遍历而言,结点的信息需要保留在栈中,因为访问右子树前需要通过栈进入右子树,访问右子树后还要访问结点。
因此不同之处在于,结点信息保存在栈中的先后。
3、先序遍历二叉树的非递归实现
(1)、原理
步骤1、访问根结点,根结点入栈,然后进入左子树,访问左子树的结点并入栈,再进入下一层左子树,……如此重复,直到结点为空
步骤2、如果栈非空,则退栈顶结点,访问该栈顶结点的根结点,进入其右子树。
重复步骤1、2,直到栈和当前结点都为空,结束。
用例图解释执行操作
步骤1:
访问根结点 1并入栈,
进入左子树,访问左子树根结点 2 并入栈,继续进入 2 的左子树,访问其根结点 4 并入栈,继续进入 4 的左子树,访问其根结点 8 并入栈,此时结点为空(度为0)。
步骤2:
栈非空
退栈顶结点 8,访问 8 的根结点(因之前栈中保存了根结点4的信息,可直接用),通过根结点取右孩子的指针,进入根结点 4 的右子树,
继续步骤1:
访问其右子树的根结点 9 并入栈
进入根结点 9 左子树,结点空,退栈顶结点 9,并进入栈顶结点 9 的右子树,右子树也为空。
此时右子树以及结束,退栈顶结点4,直接退到再上层,访问左子树 4 的根结点 2(前面结点2以及入栈,直接使用)。
进入根结点 2 的右子树
继续步骤1:
重复步骤!
(2)、算法实现
算法简单概括:
从根开始,当前结点存在或栈不为空,重复如下操作
1、访问根结点,进栈,进入其左子树,重复直至当前结点为空。
2、如果栈非空,则退栈顶结点,并进入右子树。
void PreOrder(BiTree root)
{
Sepstack*S; //指向栈顶的指针
BiTree p; //指向树的指针
InitStack(S); //对栈初始化操作
p=root; //p指针指向树的根结点
while(p!=NULL|| !IsEmpty(S)) // 判断重复执行条件
{
while(p!=NULL) //结点非空执行步骤1
{
visit(p->data);
Push(S,p); //将指针p指向的根结点进栈
p=p->Lchild;
}
if(!IsEmpty(S)) // 栈非空,执行步骤2
{
Pop(S,&p); //退栈顶结点
p=p->Rchild;
}
}
4、中序遍历二叉树的非递归实现
(1)、原理
步骤1、根结点入栈,进入其左子树,进入左子树的根结点并入栈,继续进入其左子树……重复操作,直到结点为空
步骤2、若栈非空,从栈顶退出栈顶结点,访问出栈结点(注意与先序遍历的区别,此时访问根结点),并进入其右子树
(2)、算法实现
算法简单概括:
从根开始,当前结点不为空或栈不为空,重复如下操作:
1、当前结点进栈,进入左子树,重复直至结点为空。
2、若栈非空,则退栈,访问出栈结点,并进入其右子树。
void InOrder(BiTree root)
{
Sepstack*S; //指向栈顶的指针
BiTree p; //指向树的指针
InitStack(S); //对栈初始化操作
p=root; //p指针指向树的根结点
while(p!=NULL|| !IsEmpty(S)) // 判断重复执行条件
{
while(p!=NULL) //结点非空执行步骤1
{
Push(S,p); //将指针p指向的根结点进栈
p=p->Lchild;
}
if(!IsEmpty(S)) // 栈非空,执行步骤2
{
Pop(S,&p); //退栈顶结点
Visit(p->data); //访问出栈结点
p=p->Rchild;
}
}
5、后序遍历二叉树的非递归实现
(1)、原理
后序遍历与 其他两种遍历不同的是:
后序遍历中,左右子树遍历结束后,从右子树返回,上一层的结点才能退栈并访问。
从子树返回时,如何判别,是从左子树还是右子树返回,以便确定栈顶的上一层结点?
或者如果子树的根结点只有左子树,没有右子树时又怎么访问根结点呢?
从子树返回时,需要判断栈顶结点 p 的右子树是否为空,或刚访问的结点 q 是不是 p 的右孩子。
如果是:
说明 p 无右子树,或右子树刚访问过,应退栈,访问出栈的 p 结点,并将 p 赋给 q(q始终记录刚访问的结点),然后将 p 置空(避免再次进入该棵树访问);
如果不是:
说明 p 有右子树,或右子树未访问,应进入右子树进行访问。
例图:第一次手画,见谅,有经验之后会逐渐改善!
综上,后序遍历的实现过程为:
步骤1、
根结点入栈,进入其左子树,进入左子树的根结点并入栈,进入下一层左子树……重复,直到结点为空
步骤2、
若栈非空,
如果栈顶结点p的右子树为空,或其右孩子是刚访问过的 q ,退栈、访问p 结点,并将 p 赋给q,p置空;
如果p有右子树,且右子树未访问,进入p的右子树。
重复1、2步骤,直至当前结点及栈均为空
(2)、算法实现
算法简单概括:
从根开始,当前结点存在或栈不为空,重复操作:
1、当前结点进栈,进入其左子树,入栈,重复直至结点为空
2、栈非空,判栈顶结点的右子树是否存在、右子树是否刚访问过。是,则退栈,访问p结点,将p赋给q,p置空;否,进入p的右子树。
void PostOrder(Bitree root)
{
Seqstack*S;
BiTree p,q; // 定义两个指向树的指针
Initstack(S); //初始化栈
p=root; //p指针指向根结点
q=NULL; //始终指向访问过结点的指针置空
while(p!=NULL||!IsEmpty(S)) //执行条件,结点存在,栈非空
{
while(p!=NULL) //步骤1
{
Push(S,p); //进栈
p=p->Lchild; //进入左子树
}
if(!IsEmpty(S)) //栈非空
{
Top(S,&p); // 获取栈顶元素赋值给p,判断p的情况
if((p->Rchild==NULL)||(p->Rchild==q)) // 步骤2判断条件
{
Pop(S,&p); //出栈
Visit(p->data); //访问出栈结点
q=p; //q标记访问过p
p=NULL; //p置空
}
else
p=p->Rchild; //进入右子树
}
}
}
6、二叉树的层次遍历
(1)、原理
如果有些要求结构的层次性,需要讨论先上(根)后下(子)的层次遍历。
二叉树的层次遍历,是指从二叉树的第一层(根结点)开始,自上而下逐层遍历,同层内按照从左往右的顺序逐个结点访问。
如图:
层次遍历的结点次序为:
A、B、C、D、E、F、G、H.
如何实现这样的遍历访问呢?
当一层访问完后,按访问的次序,再对各结点的左、右孩子进行访问,(即对下一层从左到右访问)。
访问的特点是:先访问的结点,其孩子也先访问,与队列的控制特点吻合,先访问的结点,其孩子先进入队列,后访问的结点,其孩子后进入队列,先进先出,便可实现每一层从左到右的次序依次访问。
(2)、算法实现
算法简单概括:
1、队头结点入队,并访问出队结点。
2、出队结点的非空左、右孩子依次入队。
void LevelOrder(BiTree root)
{
SeqQueue*Q; //指向队列的指针
BiTree p; //指向树的指针
InitQueue(Q); //初始化队列
EnterQueue(Q,root); //根结点(第一层)入队
while(!IsEmpty(Q)) //队列不为空
{
DeleteQueue(Q,&p); //出队
visit(p->data); //访问出队的结点
if(p->Lchild!=NULL) //出队结点的左孩子存在入队
EnterQueue(Q,p->Lchild);
if(p->Rchild!=NULL) //右孩子存在入队
EnterQueue(Q,p->Rchild);
}
}