二叉树的遍历
遍历二叉树,就是按一定的某条搜索路径走遍二叉树的所有结点,使每一个结点都被访问一次,而且只被访问一次。由于二叉树是非线性结构,每个结点都有可能有两棵子树,因此,树的遍历实质上是将二叉树的各个结点转换成为一个线性序列来表示。
递归遍历
由二叉树的递归定义可知,遍历一棵二叉树便要决定对根节点N,左子树L和右子树R的访问顺序,所以按照先遍历左子树后遍历右子树的原则,常见的遍历次序有先序(NLR),中序(LNR)和后序(LRN)三种遍历算法;
【注】“序”指的是遍历过程中根节点何时被访问。
下面分别介绍一下这三种遍历的操作过程
1.先序遍历
若二叉树为空,则什么也不做,否则依次执行如下操作:
(1)访问根结点;
(2)先序遍历左子树;
(3)先序遍历右子树;
对应的递归算法如下:
void PreOrder(BiTree T){
if(T != NULL){
visit(T); //访问根节点
PreOrder(T->lchild); //递归遍历左子树
PreOrder(T->rchild); //递归遍历右子树
}
}
1.中序遍历
若二叉树为空,则什么也不做,否则依次执行如下操作:
(1) 中序遍历左子树;
(2)访问根结点;
(3) 中序遍历右子树;
对应的递归算法如下:
void InOrder(BiTree T){
if(T != NULL){
InOrder(T->lchild); //递归遍历左子树
visit(T); //访问根节点
InOrder(T->rchild); //递归遍历右子树
}
}
1.后序遍历
若二叉树为空,则什么也不做,否则依次执行如下操作:
(1)后序遍历左子树;
(2)后序遍历右子树;
(3)访问根结点;
对应的递归算法如下:
void PostOrder(BiTree T){
if(T != NULL){
PostOrder(T->lchild); //递归遍历左子树
PostOrder(T->rchild); //递归遍历右子树
visit(T); //访问根节点
}
}
下面根据上面三种遍历算法看一个例子:
箭头的虚线表示3种算法的递归执行过程,其中向下的箭头表示更深一点的递归调用,向上的箭头表示从递归调用退出返回;虚线旁的三角形,圆形,方形内的字符分别表示在先序,中序和后序遍历过程种访问结点输出的信息
根据上面的算法可以知道,这三种遍历访问图中结点的顺序为:
先序遍历:A B D E C;
中序遍历:D B E A C;
后序遍历:D E B C A;
从上面我们可以看出这三种遍历算法中,递归遍历左,右子树的顺序都是一样的,只是访问根节点的顺序不同。每个结点都访问了且仅访问一次,所以其时间复杂度为O(n);
非递归遍历
递归方法是相对简单的,这是由二叉树的结构决定的;但非递归的思路也是比较简单的只是算法实现相对麻烦很多;那么根据递归变非递归的方法,我们可以借用栈来解决这个问题。
1.先序遍历
我们将树的每个节点压入栈中,由于是先序遍历,首先压入的是根节点,然后弹出(弹出节点时打印信息,且一个循环弹出一个节点),接着是压入右子树节点,最后压入左子树节点。(堆栈是先进先出的)
void PreOrderTraverse(BinTree b)
{
InitStack(S); //初始化创建栈
BinTree p=b; //p为工作指针
while(p||!isEmpty(s))
{
while(p) //到最左下的孩子
{
printf(" %c ",p->date); //先序先遍历结点
Push(S,p); //入栈
p=p->lchild;
}
if(!isEmpty(s)) //在栈不为空的情况下,左孩子为空,弹出该结点,遍历右孩子
{
p=Pop(s);
p=p->rchild;
}
}
}
中序遍历
中序时,我们首先去遍历二叉树的左分支,并将节点压入栈中,只到找到最左边的叶节点,接着弹出(并打印节点),并看其有没右分支,如果没有,栈再弹出一个节点(根节点),看其有没有右分支。每次弹出,都要观察其是否有右分支,也就是说每个节点都遍历了两次!
void InOrderTraverse(BinTree b)
{
InitStack(S); //初始化创建栈
BinTree p=b; //p为工作指针
while(p||!isEmpty(s))
{
while(p)
{
Push(S,p); //中序现将结点进栈保存
p=p->lchild;
} //遍历到左下角尽头再出栈访问
if(!isEmpty(s)) //在栈不为空的情况下,左孩子为空,弹出该结点,遍历右孩子
{
p=Pop(s);
printf(" %c ",p->data);
p=p->rchild; //遍历右孩子
}
}
}
后序遍历
后序遍历在意思上和前序遍历相近,而前序遍历的压栈顺序为:根、右、左。那么如果我们使用两个堆栈,第一个压栈顺序为:根、左、右,但是在(先序遍历时)弹出根节点时将根节点压入第二个堆栈,为什么这里压栈顺序要为左右呢?很简单,在第一个堆栈中最后压入右子树,那么右子树会最先压入第二个堆栈,相应的,当第二个堆栈弹出时,右子树会在左子树的后面弹出(先进后出)。注意:根节点是最先被压入第一个栈中的,同时也是最先被压入第二个栈中的!
void PostOrderTraverse(BinTree b)
{
InitStack(S); //初始化创建栈
BinTree p=b, r=NULL; //p为工作指针,辅助指针r
while(p||!isEmpty(s))
{
if(p) //从根节点到最左下角的左子树都入栈
{
Push(S,p); //中序现将结点进栈保存
p=p->lchild;
}
else
{
GetTop(S,p); //取栈顶,注意!不是出栈!
if(p->rchild&&p->rchild!=r) //右子树还没有访问并且右子树不空,第一次栈顶
{
p=p->rchild; //进入右子树
}
else //右子树已经访问或为空,接下来出栈访问结点,第二次栈顶
{
p=Pop(s);
printf(" %c ",p->data);
r=p; //指向访问过的右子树结点
p=NULL; //使p为空继续访问栈顶
}
}
}
}
层序遍历
如下图所示为二叉树的层序遍历,即按照箭头所示的方向,一层一层的访问二叉树的结点。
要进行层序遍历,需要借助一个队列,先将二叉树的根节点入队列,然后出队,访问出队结点,若它有左子树,先将左子树根节点入队;若它有右子树,则右子树根节点入队。然后出队,访问队结点…如此循环反复,直至队列为空。
二叉树的层序遍历算法如下:
void LevelOrder(BiTree T){
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q,T); //将根节点入队;
while(!IsEmpty(Q)){ //队列不为空则循环
DeQueue(Q,p) //对头结点出队
visit(p); //访问出队结点
if(p->lchild != NULL)
EnQueue(Q,p->lchild); //左孩子不空,左子树根节点入队
if(p->rchild != NULL)
EnQueue(Q,p->rchild); //右孩子不空,右子树根节点入队
}
}