所谓二叉树的遍历,就是按照某种次序访问二叉树中每个节点,而且每个节点仅访问一次的过程。以L,N,R分别表示遍历左子树、访问根节点和遍历右子树,则可能有NLR、LNR、LRN、NRL、RNL、RLN等六种遍历方式。若限定先左后右,则只有前三种遍历,分别称为先序遍历、中序遍历、后序遍历,另外还有一种层序的遍历方式。
由于遍历的递归算法比较简单,这里只介绍遍历的非递归算法。这里给二叉链存储结构中节点类型给出如下定义便于后面代码书写,定义如下:
typedef struct node
{
ElementType data;//数据元素
struct node *lchild;//指向左孩子节点
struct node *rchild;//指向右孩子节点
} BTNode;
下面以如图所示的二叉树对应的二叉链存储结构为例讲解:
1.先序遍历
a.访问根节点
b.先序遍历左子树。
c.先序遍历右子树。
由先序遍历过程可知,先访问根节点,再访问左子树,最后访问右子树,因此,先将根节点进栈,在栈不空时循环:出栈p,访问*p节点,将其右孩子节点进栈,再将左孩子节点进栈。(简述:先根进栈 出栈 再右子树进栈 左子树进栈)
先序遍历的非递归算法如下:
void PreOrder(BTNode *p)//先序遍历的非递归算法
{
BTNode *St[MaxSize],*p;//顺序栈St中保存的是节点指针,而不是节点值
int top=-1;
if(b!=NULL)
{
top++;//根节点进栈
St[top]=b;
while(top>-1)//栈不为空时循环
{
p=St[top];//退栈并访问该节点
top--;
printf("%c",p->data);
if(p->rchild!=NULL)//有左孩子节点时将其进栈
{
top++;
St[top]=p->rchild;
}
if(p->rchild!=NULL)//有右孩子节点时将其进栈
{
top++;
St[top]=p->lchild;
}
}
printf("\n");
}
}
先序遍历后的结果为:A B D G C E F
2.中序遍历
a.中序遍历左子树
b.访问根节点
c.中序遍历右子树
由中序遍历的过程可知,采用一个栈保存需要返回的节点指针。先扫描(并非访问)根节点的所有左节点并将他们一一进栈。然后出栈一个节点*p,显然*p节点没有左孩子子节点或者左孩子节点并访问过(表明该节点的左子树均已访问)则访问它。然后扫描该节点的右孩子节点,将其进栈,再扫描该右孩子节点的所有左节点并一一进栈,如此这样,直到栈空为止。( 简述:先所有的左节点进栈,出栈一个节点, 右节点进栈)
对应的非递归算法如下:
中序遍历的非递归算法
void InOrder(BTNode *b)
//中序遍历的非递归算法
{
BTNode *St[MaxSize],*p;//顺序栈St中保存的是节点指针
int top=-1;
if(b!=NULL)
{
p=b;
while(top>-1 || p!=NULL)//栈不为空或p不空时循环
{
while(p!=NULL)//扫描*p的所有左节点并进栈
{
top++;
St[top]=p;
p=p->lchild;
}
if(top>-1)
{
p=St[top];//出栈*p节点,它没有右孩子或右子树已访问
top--;
printf("%c",p->data);//访问之
p=p->rchild;//扫描*p的右孩子节点
}
}
printf("\n");
}
中序遍历后的结果为:D G B A E C F
3.后续遍历
后续遍历二叉树过程如下:
a.后序遍历左子树
b.后续遍历右子树
c.访问根节点
由后序遍历的过程可知,采用一个栈保存需要返回的节点指针,先扫描根节点的所有左节点并一一进栈,出栈一个节点*p,即当前节点,然后扫描该节点的右孩子节点并进栈,再扫描该右孩子节点的所有左节点并进栈。当一个节点的左、右孩子节点均访问后再访问该节点,如此这样,直到栈空为止。
其中的难点是如何判断一个节点*p的右孩子节点已访问过,为此用q保存右子树中刚刚访问过的节点(初值为NULL),若p->rchild==q成立(在后序序列中,*p的右孩子节点*q一定刚好在*p之前访问),说明*p节点的左、右子树均已访问,现在应访问*p节点。(简述:先将左节点进栈,出栈一个节点,右孩子进栈)
从上述过程可知,栈中保存的是当前节点*p的所有祖先节点(均未访问过)。对应的非递归算法如下:
void PostOrder(BTNode *b)//后序遍历的非递归算法
{
BTNode *St[MaxSIze];//顺序栈St中保存的是节点指针
BTNode *p=b,*q;
int flag,top=-1;//栈指针置初值
if(b!=NULL)
{
do
{
while(p!=NULL)//将*p的所有左节点进栈
{
top++;
St[top]=p;
p=p->lchild;
}
q=NULL;//q指向栈顶节点的前一个已访问的节点
flag=1;//设置flag=1表示处理栈顶节点
while(top!=-1&&flag==1)
{
p=St[top];//取出当前的栈顶元素
if(p->rchild==q)//右孩子不存或右孩子已被访问,访问之
{
printf("%c",p->data);//访问*p节点
top--;
q=p;//让q指向被刚被访问的节点
}
else
{
p=p->rchild;//p指向右孩子节点
flag=0;//设置flag=0表示栈顶处理完毕
}
}
}while(top!=-1);//栈不为空时循环
printf("\n");
}
}
中序遍历后的结果为:DGBAECF
注意:当访问一个节点*p时,栈中节点恰好是*q节点的所有祖先。从栈底到栈顶节点再加上*q节点,刚好构成从根节点到*p节点的一条路径。在很多算法设计中都利用这一特性求解,如求从根节点到某节点的路径算法、求两个节点的最近公共祖先算法等都可以用这个思想来实现。
4.层次遍历
层次遍历的过程如下:
a.访问根节点(第一层)。
b.从左到右访问第二层的所有节点。
c.从左到右访问第三层的所有节点,…,第h层的所有节点
对应的层次遍历算法如下:
void LevelOrder(BTNode *b)//层序遍历的非递归算法
{
BTNode *p;
BTNode *qu[MaxSize]; //定义循环队列,存放节点指针
int front,rear;
//定义队头和队尾指针
front=rear=0;//置队列为空队列
rear++;
qu[rear]=b;//根节点指针入队
while(front!=rear)
{
front=(front+1)%MaxSize;//队头出队
p=qu[front];//访问节点
printf("%c",p->data);
if(p->lchild!=NULL)//有左孩子时将其进队
{
rear=(rear+1)%MaxSize;
qu[rear]=p->lchild;
}
if(p->rchild!=NULL)//有右孩子时将其进队
{
rear=(rear+1)%MaxSize;
qu[rear]=p->rchild;
}
}
}
层序遍历的结果为:ABCDEFG