前言
最近看到二叉树,因为二叉树的内容比较多,就把遍历单独总结成一篇博客,其他内容后面再写。
首先明确一下遍历的意义:二叉树有根结点、左子树和右子树三个部分,若能按一定的顺序访问这三个部分,就是遍历了整个二叉树。假如访问的先后顺序可以随机,一共会有六种可能性。
现在我们规定,只能先访问左子树,再访问右子树,就只剩下三种情况:根结点->左子树->右子树,左子树->根结点->右子树, 左子树->右子树->根结点。
这就是二叉树的先序、中序、后序遍历,这个所谓的 “序” 就是指根结点的访问次序。 “访问” 指的是对结点真正进行操作,例如输出结点数据、修改结点信息等。因为规定了访问的次序,可能会出现我们到达了某个结点,但是不进行任何操作,等下次到达再操作的情况。
注意,这个 “根结点” 是相对的概念,例如对下图的B结点而言,它既是A结点的左子树,也是C、D结点的根结点。也就是说,假设我们使用中序遍历,找到A的左子树B结点;由于B结点同时也是根结点,因此不能直接访问,而是接着寻找到B的左子树C。
同时,对二叉树的遍历也可以按从上至下、从左至右来进行。这样我们是 “一层一层” 地来访问二叉树的结点,称为层次遍历。
三种按次序的遍历都有递归和非递归两种实现方式,再加上层次遍历(本身就是非递归的),因此一共有七种实现遍历二叉树的方法。
先序遍历
先序遍历二叉树的操作是:若二叉树为空,则结束遍历;否则先访问根结点,然后先序遍历左子树,再先序遍历右子树。(左右子树同样是二叉树,遍历时也按照先序遍历的方式进行。)
递归方法
用递归方法实现先序遍历非常简单,因为对左右子树同样采取先序遍历的方式,只需要调用同一个方法即可。
//先序遍历-递归
void PreOrder(BinTreeNode *t){
if (t != NULL) {
printf("%4c",t->data);
PreOrder(t->leftChild);
PreOrder(t->rightChild);
}
}
我们来看看代码执行时的状况:
-
参数传入根结点A,A不为空,输出A结点的数据,再次调用先序遍历方法,传入A的左子树B
-
参数传入根结点B,B不为空,输出B结点的数据,再次调用先序遍历方法,传入B的左子树C
-
参数传入根结点C,C不为空,输出C结点的数据,再次调用先序遍历方法,传入C的左子树NULL
-
参数传入NULL,if 语句不执行,该方法结束。
-
再次调用先序遍历方法,传入C的右子树NULL
-
参数传入NULL,if 语句不执行,该方法结束。此时,③处的方法也执行完毕了,回到②处
-
再次调用先序遍历方法,传入B的右子树D
-
参数传入根结点D,D不为空,输出D结点的数据,再次调用先序遍历方法,传入D的左子树E
-
参数传入根结点E, E不为空,输出E结点的数据, 再次调用先序遍历方法,传入E的左子树NULL
-
同4~6,左右子树为空,不执行语句,⑤处方法执行完毕,回到④处,传入D的右子树
-
参数传入根结点F, F不为空,输出F结点的数据, 再次调用先序遍历方法,传入F的左子树
-
同4~6,左右子树为空,不执行语句,⑥处方法执行完毕,同时,④处、②处方法也执行完毕
- 回到①处,传入A的右子树G,后续的照推即可。
非递归方法
我们知道,递归方法是可以转换为非递归方法的。观察上面的递归过程,不难发现一点:递归方法的处理顺序,是遵循先入后出规律的。最先执行的A结点方法,是在最后被处理完的。而 “先入后出” 一词很容易让我们想到栈结构。
实际上在计算机中,函数(方法)的调用就是通过栈来实现的,递归函数自然也不例外。只是,系统所维护的这个函数栈是有一定大小的,因此递归函数隐含着一个问题:递归的次数过多,会造成栈的溢出。
我们想将递归方法转为非递归方法,也离不开栈结构,不过这个栈结构不是系统设计的,而是我们自己设计维护的。这里使用的栈结构是之前写好的顺序栈,见之前的博客栈结构之顺序栈。
在之前的顺序栈中,结点的类型是int型,因此要修改一下才能保存二叉树的结点。
struct BinTreeNode;
#define EType BinTreeNode*
typedef struct SeqStack{
struct EType *base;
//栈容量
int capacity;
//表示栈顶所在的位置,也表示了当前栈内元素的个数
int top;
}SeqStack;
修改完之后就可以直接调用之前写好的方法了。现在考虑:怎么用栈结构实现先序遍历的非递归?
其实,利用栈结构对二叉树进行遍历,无非就是一个个结点先入栈再出栈的过程。每个结点出栈时,执行对结点的 访问操作,这样来输出遍历序列(出栈之前,通过获取栈顶元素的方法来得到即将出栈的结点,就可以实现访问了)。而出栈的顺序是由入栈的顺序决定的,也就是说,最后输出的序列是先序、中序还是后序,重点在入栈的顺序。
下面看先序遍历时的入栈顺序。需要明确:
- 整棵树的根结点必然先入栈
- 因为是先访问左子树,再访问右子树,意味着左子树先出栈,右子树后出栈,因此入栈时先右后左
- 空结点不入栈
代码如下:
//先序遍历-非递归
void PreOrder_2(BinTreeNode *t){
if (t != NULL) {
SeqStack Q;
InitStack(&Q);
//根结点入栈
Push(&Q, t);
BinTreeNode *s