6.3 二叉树的遍历
二叉树的遍历方法及递归实现
遍历是二叉树中经常要用到的一种操作。因为在实际应用问题中,常常需要按一定顺序对二叉树中的每个结点逐个进行访问,查找具有某一特点的结点,然后对这些满足条件的结点进行处理。
由二叉树的定义可知,一棵由根结点、根结点的左子树和根结点的右子树三部分组成。
因此,只要依次遍历这三部分,就可以遍历整个二叉树。若以D、L、R 分别表示访问根结点、遍历根结点的左子树、遍历根结点的右子树,则二叉树的遍历方式有六种:DLR、LDR、LRD、DRL、RDL 和RLD。如果限定先左后右,则只有前三种方式,即DLR(称为先序遍历)、LDR(称为中序遍历)和LRD(称为后序遍历)。
1.先序遍历(DLR)
先序遍历的递归过程为:若二叉树为空,遍历结束。否则,
(2)先序遍历根结点的左子树;
(3)先序遍历根结点的右子树。
先序遍历二叉树的递归算法如下:
1 void PreOrder(BiTree bt) 2 { 3 /*先序遍历二叉树bt*/ 4 if (bt==NULL) /*递归调用的结束条件*/ 5 return; 6 Visite(bt->data); /*访问结点的数据域*/ 7 PreOrder(bt->lchild); /*先序递归遍历bt 的左子树*/ 8 PreOrder(bt->rchild); /*先序递归遍历bt 的右子树*/ 9 }
2.中序遍历(LDR)
(1)中序遍历根结点的左子树;
(2)访问根结点;
(3)中序遍历根结点的右子树。
中序遍历二叉树的递归算法如下:
1 void InOrder(BiTree bt) 2 { 3 /*中序遍历二叉树bt*/ 4 if (bt==NULL) 5 return; /*递归调用的结束条件*/ 6 InOrder(bt->lchild); /*中序递归遍历bt 的左子树*/ 7 Visite(bt->data); /*访问结点的数据域*/ 8 InOrder(bt->rchild); /*中序递归遍历bt 的右子树*/ 9 }
3.后序遍历(LRD)
(1)后序遍历根结点的左子树;
(2)后序遍历根结点的右子树。
(3)访问根结点;
后序遍历二叉树的递归算法如下:
1 void PostOrder(BiTree bt) 2 { 3 /*后序遍历二叉树bt*/ 4 if (bt==NULL) 5 return; /*递归调用的结束条件*/ 6 PostOrder(bt->lchild); /*后序递归遍历bt 的左子树*/ 7 PostOrder(bt->rchild); /*后序递归遍历bt 的右子树*/ 8 Visite(bt->data); /*访问结点的数据域*/ 9 }
4.层次遍历
下面讨论层次遍历的算法。
由层次遍历的定义可以推知,在进行层次遍历时,对一层结点访问完后,再按照它们的访问次序对各个结点的左孩子和右孩子顺序访问,这样一层一层进行,先遇到的结点先访问,这与队列的操作原则比较吻合。因此,在进行层次遍历时,可设置一个队列结构,遍历从二叉树的根结点开始,首先将根结点指针入队列,然后从对头取出一个元素,每取一个元素,执行下面两个操作:
(1)访问该元素所指结点;
(2)若该元素所指结点的左、右孩子结点非空,则将该元素所指结点的左孩子指针和右孩子指针顺序入队。
此过程不断进行,当队列为空时,二叉树的层次遍历结束。在下面的层次遍历算法中,二叉树以二叉链表存放,一维数组Queue[MAXNODE]用以实现队列,变量front 和rear 分别表示当前队首元素和队尾元素在数组中的位置。
1 void LevelOrder(BiTree bt) 2 /*层次遍历二叉树bt*/ 3 { 4 BiTree Queue[MAXNODE]; 5 int front,rear; 6 if (bt==NULL) 7 return; 8 front=-1; 9 rear=0; 10 queue[rear]=bt; 11 while(front!=rear) 12 { 13 front++; 14 Visite(queue[front]->data); /*访问队首结点的数据域*/ 15 if (queue[front]->lchild!=NULL) /*将队首结点的左孩子结点入队列*/ 16 { 17 rear++; 18 queue[rear]=queue[front]->lchild; 19 } 20 if (queue[front]->rchild!=NULL) /*将队首结点的右孩子结点入队列*/ 21 { 22 rear++; 23 queue[rear]=queue[front]->rchild; 24 } 25 } 26 }
二叉树遍历的非递归实现
前面给出的二叉树先序、中序和后序三种遍历算法都是递归算法。当给出二叉树的链式存储结构以后,用具有递归功能的程序设计语言很方便就能实现上述算法。然而,并非所有程序设计语言都允许递归;另一方面,递归程序虽然简洁,但可读性一般不好,执行效率也不高。因此,就存在如何把一个递归算法转化为非递归算法的问题。解决这个问题的方法可以通过对三种遍历方法的实质过程的分析得到。
如图6.3(b)所示的二叉树,对其进行先序、中序和后序遍历都是从根结点A 开始的,且在遍历过程中经过结点的路线是一样的,只是访问的时机不同而已。图6.9 中所示的从根结点左外侧开始,由根结点右外侧结束的曲线,为遍历图6.3(b)的路线。
沿着该路线按△标记的结点读得的序列为先序序列,按*标记读得的序列为中序序列,按⊕标记读得的序列为后序序列。
然而,这一路线正是从根结点开始沿左子树深入下去,当深入到最左端,无法再深入下去时,则返回,再逐一进入刚才深入时遇到结点的右子树,再进行如此的深入和返回,直到最后从根结点的右子树返回到根结点为止。
先序遍历是在深入时遇到结点就访问,中序遍历是在从左子树返回时遇到结点访问,后序遍历是在从右子树返回时遇到结点访问。
在这一过程中,返回结点的顺序与深入结点的顺序相反,即后深入先返回,正好符合栈结构后进先出的特点。因此,可以用栈来帮助实现这一遍历路线。
其过程如下:
在沿左子树深入时,深入一个结点入栈一个结点。
若为先序遍历,则在入栈之前访问之;
当沿左分支深入不下去时,则返回,即从堆栈中弹出前面压入的结点,若为中序遍历,则此时访问该结点,然后从该结点的右子树继续深入;
若为后序遍历,则将此结点再次入栈,然后从该结点的右子树继续深入,与前面类同,仍为深入一个结点入栈一个结点,深入不下去再返回,直到第二次从栈里弹出该结点,才访问之。
(1)先序遍历的非递归实现
在下面算法中,二叉树以二叉链表存放,一维数组stack[MAXNODE]用以实现栈,变量top 用来表示当前栈顶的位置。
1 void NRPreOrder(BiTree bt) 2 { 3 /*非递归先序遍历二叉树*/ 4 BiTree stack[MAXNODE],p; 5 int top; 6 if (bt==NULL) 7 return; 8 top=0; 9 p=bt; 10 while(!(p==NULL&&top==0)) 11 { 12 while(p!=NULL) 13 { 14 Visite(p->data); /*访问结点的数据域*/ 15 if (top<MAXNODE-1) /*将当前指针p 压栈*/ 16 { 17 stack[top]=p; 18 top++; 19 } 20 else 21 { 22 printf(“栈溢出”); 23 return; 24 } 25 p=p->lchild; /*指针指向p 的左孩子*/ 26 } 27 if (top<=0) /*栈空时结束*/ 28 return; 29 else 30 { 31 top--; 32 p=stack[top]; /*从栈中弹出栈顶元素*/ 33 p=p->rchild; /*指针指向p 的右孩子结点*/ 34 } 35 } 36 }
算法6.9
对于图6.3(b)所示的二叉树,用该算法进行遍历过程中,栈stack 和当前指针p 的变化情况以及树中各结点的访问次序如表6.1 所示。
(2)中序遍历的非递归实现
中序遍历的非递归算法的实现,只需将先序遍历的非递归算法中的Visite(p->data)移到p=stack[top]和p=p->rchild 之间即可。
(3)后序遍历的非递归实现
由前面的讨论可知,后序遍历与先序遍历和中序遍历不同,在后序遍历过程中,结点在第一次出栈后,还需再次入栈,也就是说,结点要入两次栈,出两次栈,而访问结点是在第二次出栈时访问。因此,为了区别同一个结点指针的两次出栈,设置一标志flag,令:
当结点指针进、出栈时,其标志flag 也同时进、出栈。因此,可将栈中元素的数据类型定义为指针和标志flag 合并的结构体类型。定义如下:
1 typedef struct 2 { 3 BiTree link; 4 int flag; 5 }stacktype;
后序遍历二叉树的非递归算法如下。在算法中,一维数组stack[MAXNODE]用于实现栈的结构,指针变量p 指向当前要处理的结点,整型变量top 用来表示当前栈顶的位置,整型变量sign 为结点p 的标志量。
1 void NRPostOrder(BiTree bt) 2 /*非递归后序遍历二叉树bt*/ 3 { 4 stacktype stack[MAXNODE]; 5 BiTree p; 6 int top,sign; 7 if (bt==NULL) 8 return; 9 top=-1 /*栈顶位置初始化*/ 10 p=bt; 11 while (!(p==NULL && top==-1)) 12 { 13 if (p!=NULL) /*结点第一次进栈*/ 14 { 15 top++; 16 stack[top].link=p; 17 stack[top].flag=1; 18 p=p->lchild; /*找该结点的左孩子*/ 19 } 20 else 21 { 22 p=stack[top].link; 23 sign=stack[top].flag; 24 top--; 25 if (sign==1) /*结点第二次进栈*/ 26 { 27 top++; 28 stack[top].link=p; 29 stack[top].flag=2; /*标记第二次出栈*/ 30 p=p->rchild; 31 } 32 else 33 { 34 Visite(p->data); /*访问该结点数据域值*/ 35 } 36 } 37 } 38 }
算法6.10
由遍历序列恢复二叉树
从前面讨论的二叉树的遍历知道,任意一棵二叉树结点的先序序列和中序序列都是唯一的。反过来,若已知结点的先序序列和中序序列,能否确定这棵二叉树呢?这样确定的二叉树是否是唯一的呢?回答是肯定的。
根据定义,二叉树的先序遍历是先访问根结点,其次再按先序遍历方式遍历根结点的左子树,最后按先序遍历方式遍历根结点的右子树。这就是说,在先序序列中,第一个结点一定是二叉树的根结点。
另一方面,中序遍历是先遍历左子树,然后访问根结点,最后再遍历右子树。这样,根结点在中序序列中必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,而后一个子序列是根结点的右子树的中序序列。
根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中,左子序列的第一个结点是左子树的根结点,右子序列的第一个结点是右子树的根结点。这样,就确定了二叉树的三个结点。同时,左子树和右子树的根结点又可以分别把左子序列和右子序列划分成两个子序列,如此递归下去,当取尽先序序列中的结点时,便可以得到一棵二叉树。
同样的道理,由二叉树的后序序列和中序序列也可唯一地确定一棵二叉树。因为,依据后序遍历和中序遍历的定义,后序序列的最后一个结点,就如同先序序列的第一个结点一样,可将中序序列分成两个子序列,分别为这个结点的左子树的中序序列和右子树的中序序列,再拿出后序序列的倒数第二个结点,并继续分割中序序列,如此递归下去,当倒着取尽后序序列中的结点时,便可以得到一棵二叉树。下面通过一个例子,来给出右二叉树的先序序列和中序序列构造唯一的一棵二叉树的实现算法。
已知一棵二叉树的先序序列与中序序列分别为:
A B C D E F G H I
B C A E D G H F I
试恢复该二叉树。
首先,由先序序列可知,结点A 是二叉树的根结点。
其次,根据中序序列,在A 之前的所有结点都是根结点左子树的结点,在A 之后的所有结点都是根结点右子树的结点,由此得到图6.10 (a)所示的状态。
然后,再对左子树进行分解,得知B 是左子树的根结点,又从中序序列知道,B 的左子树为空,B 的右子树只有一个结点C。
接着对A 的右子树进行分解,得知A 的右子树的根结点为D;而结点D 把其余结点分成两部分,即左子树为E,右子树为F、G、H、I,如图6.10 (b)所示。
接下去的工作就是按上述原则对D 的右子树继续分解下去,最后得到如图6.10 (c)的整棵二叉树。
上述过程是一个递归过程,其递归算法的思想是:先根据先序序列的第一个元素建立根结点;然后在中序序列中找到该元素,确定根结点的左、右子树的中序序列;再在先序序列中确定左、右子树的先序序列;最后由左子树的先序序列与中序序列建立左子树,由右子树的先序序列与中序序列建立右子树。
下面给出用C 语言描述的该算法。假设二叉树的先序序列和中序序列分别存放在一维数组preod[ ]与inod[ ]中,并假设二叉树各结点的数据值均不相同。
1 void ReBiTree(char preod[],char inod[],int n,BiTree root) 2 /*n 为二叉树的结点个数,root 为二叉树根结点的存储地址*/ 3 { 4 if (n≤0) 5 root=NULL; 6 else 7 PreInOd(preod,inod,1,n,1,n,&root); 8 }
算法6.11
1 void PreInOd(char preod[],char inod[],int i,j,k,h,BiTree *t) 2 { 3 *t=(BiTNode *)malloc(sizeof(BiTNode)); 4 *t->data=preod[i]; 5 m=k; 6 while (inod[m]!=preod[i]) 7 m++; 8 if (m==k) 9 *t->lchild=NULL 10 else 11 PreInOd(preod,inod,i+1,i+m-k,k,m-1,&t->lchild); 12 if (m==h) 13 *t->rchild=NULL 14 else 15 PreInOd(preod,inod,i+m-k+1,j,m+1,h,&t->rchild); 16 }
算法6.12
需要说明的是,数组preod 和inod 的元素类型可根据实际需要来设定,这里设为字符型。
另外,如果只知道二叉树的先序序列和后序序列,则不能唯一地确定一棵二叉树。
不用栈的二叉树遍历的非递归方法
前面介绍的二叉树的遍历算法可分为两类,一类是依据二叉树结构的递归性,采用递归调用的方式来实现;另一类则是通过堆栈或队列来辅助实现。采用这两类方法对二叉树进行遍历时,递归调用和栈的使用都带来额外空间增加,递归调用的深度和栈的大小是动态变化的,都与二叉树的高度有关。因此,在最坏的情况下,即二叉树退化为单支树的情况下,递归的深度或栈需要的存储空间等于二叉树中的结点数。
还有一类二叉树的遍历算法,就是不用栈也不用递归来实现。常用的不用栈的二叉树遍历的非递归方法有以下三种:
(1)对二叉树采用三叉链表存放,即在二叉树的每个结点中增加一个双亲域parent,这样,在遍历深入到不能再深入时,可沿着走过的路径回退到任何一棵子树的根结点,并再向另一方向走。由于这一方法的实现是在每个结点的存储上又增加一个双亲域,故其存储开销就会增加。
(2)采用逆转链的方法,即在遍历深入时,每深入一层,就将其再深入的孩子结点的地址取出,并将其双亲结点的地址存入,当深入不下去需返回时,可逐级取出双亲结点的地址,沿原路返回。虽然此种方法是在二叉链表上实现的,没有增加过多的存储空间,但在执行遍历的过程中改变子女指针的值,这既是以时间换取空间,同时当有几个用户同时使用这个算法时将会发生问题。
(3)在线索二叉树上的遍历,即利用具有n 个结点的二叉树中的叶子结点和一度结点的n+1 个空指针域,来存放线索,然后在这种具有线索的二叉树上遍历时,就可不需要栈,也不需要递归了。有关线索二叉树的详细内容,将在下一节中讨论。
posted on 2015-06-18 14:43 chunlanse2014 阅读(841) 评论(0) 编辑 收藏 举报