本文根据清华大学邓俊辉老师课程《数据结构》总结,课程地址 。
遍历介绍
按照事先约定的某种规则或次序,对节点各访问一次而且仅一次。与向量和列表等线性结构一样,二叉树的这类访问也统称为遍历(traversal)。
二叉树本身并不具有天然的全局次序, 故为实现遍历,需通过在各节点与其孩子之间约定某种局部次序, 间接地定义某种全局次序。
按惯例左兄弟优先于右兄弟, 若记做节点 V
,及其左、右孩子 L
和 R
,则下图所示,局
部访问的次序可有 V L R
、 L V R
和 L R V
三种选择。根据节点 V
在其中的访问次序,三种策略也相应地分别称作 先序遍历
、中序遍历
和 后序遍历
。
可以根据节点 V
次序位置进行记忆,先序遍历中 V
位于前端,中序遍历中 V
位于中间,后序遍历中 V
位于后端。
下面说一下各个遍历的迭代式实现。
先序遍历
通过先序遍历操作后,返回结果的顺序如下图所示。 注意下图是最终返回的结果展示顺序,实现方法及流程并非如此。
C++
实现代码如下:
//从当前节点出发,沿左分支不断深入,直至没有左分支的节点,沿途节点遇到后立即访问
template <typename T, typename VST> //元素类型、操作器
static void visitAlongLeftBranch(BinNodePosi(T) x, VST& visit, Stack<BinNodePosi(T)>& S) {
while (x) {
visit(x->data); //访问当前节点
S.push(x->rChild); //右孩子入栈暂存(可优化:通过判断,避免空的右孩子入栈)
x = x->lChild; //沿左分支深入一层
}
}
template <typename T, typename VST> //元素类型、操作器
void travPre_I2(BinNodePosi(T) x, VST& visit) { //二叉树先序遍历算法(迭代版)
Stack<BinNodePosi(T)> S; //辅助栈
while (true) {
visitAlongLeftBranch(x, visit, S); //从当前节点出发,逐批访问
if (S.empty()) break; //直到栈空
x = S.pop(); //弹出下一批的节点
}
}
根据上面的代码,举个例子。
上图所示的二叉树遍历,流程描述如下:
- 从节点
a
出发,沿左分支不断深入,直至没有左分支的节点,沿途节点遇到后立即访问。首先a
的右节点c
直接进栈,然后访问左节点b
; b
的右节点直接进栈,此时其为空节点,所以空节点进栈,访问b
的左节点,也为空,直接进行下一步;- 弹出栈顶空节点,再弹出
c
,将c
的右节点f
直接进栈,并访问左节点d
; - 将
d
的右节点e
直接进栈,并访问左节点 ; d
的左节点为空。接下来弹出栈顶的e
,并将e
的右节点(空节点)直接进栈,访问e
的左节点;e
的左节点为空。接下来弹出栈顶的f
,并将f
的右节点(空节点)直接进栈,访问f
的左节点g
;- 将
g
的右节点(空节点)直接进栈, 访问g
的左节点; g
的左节点为空。弹出g
的右节点(空节点),再弹出f
的右节点(空节点);- 栈为空,遍历结束。(其实上述描述的每一次循环都会做一次栈是否为空的检查)
中序遍历
通过中序遍历操作后,返回结果的顺序如下图所示。
同样需注意下图是最终返回的结果展示顺序,实现方法及流程并非如此。
C++
实现代码如下:
template <typename T> //从当前节点出发,沿左分支不断深入,直至没有左分支的节点
static void goAlongLeftBranch(BinNodePosi(T) x, Stack<BinNodePosi(T)>& S) {
while (x) { S.push(x); x = x->lChild; } //当前节点入栈后随即向左侧分支深入,迭代直到无左孩子
}
template <typename T, typename VST> //元素类型、操作器
void travIn_I1(BinNodePosi(T) x, VST& visit) { //二叉树中序遍历算法(迭代版)
Stack<BinNodePosi(T)> S; //辅助栈
while (true) {
goAlongLeftBranch(x, S); //从当前节点出发,逐批入栈
if (S.empty()) break; //直至所有节点处理完毕
x = S.pop(); visit(x->data); //弹出栈顶节点并访问之
x = x->rChild; //转向右子树
}
}
根据上面的代码,举个例子。
上图所示的二叉树遍历,流程描述如下:
- 从节点
b
出发,b
进栈S
。沿左分支不断深入,遇到节点则入栈; - 直至所有左分支节点处理完毕。(此时
S
中从上往下为a、b
); - 弹出栈
S
顶节点a
并访问之; - 转向
a
右子树。到此处截止,为一个循环体操作。接下来对a
右子树,对其重复循环体类似操作; - 但这里
a
右子树为空,所以继续弹出b
。转向b
右子树,对其进行重复循环体类似操作; - 所以
f、d、c
依次入栈,c
在栈顶。弹出c
,转向c
右子树,重复循环体; c
右子树为空。弹出d
,转向d
右子树,重复循环体。e
入栈,弹出e
,转向c
右子树,重复循环体,c
右子树为空;- 弹出
f
,转向f
右子树,重复循环体。 g
入栈,g
出栈,转向g
右子树,为空;- 此时,没有新的节点入栈,栈中也没有其他节点,终止遍历操作。
后序遍历
通过后序遍历操作后,返回结果的顺序如下图所示。
C++
实现代码如下:
template <typename T> //在以S栈顶节点为根的子树中,找到最高左侧可见叶节点
static void gotoHLVFL(Stack<BinNodePosi(T)>& S) { //沿途所遇节点依次入栈
while (BinNodePosi(T) x = S.top()) //自顶而下,反复检查当前节点(即栈顶)
if (HasLChild(*x)) { //尽可能向左
if (HasRChild(*x)) S.push(x->rChild); //若有右孩子,优先入栈
S.push(x->lChild); //然后才转至左孩子
} else //实不得已
S.push(x->rChild); //才向右
S.pop(); //返回之前,弹出栈顶的空节点
}
template <typename T, typename VST>
void travPost_I(BinNodePosi(T) x, VST& visit) { //二叉树的后序遍历(迭代版)
Stack<BinNodePosi(T)> S; //辅助栈
if (x) S.push(x); //根节点入栈
while (!S.empty()) {
if (S.top() != x->parent) //若栈顶非当前节点之父(则必为其右兄),此时需
gotoHLVFL(S); //在以其右兄为根之子树中,找到HLVFL(相当于递归深入其中)
x = S.pop(); visit(x->data); //弹出栈顶(即前一节点之后继),并访问之
}
}
根据上面的代码,举个例子。
- 找到最高左侧可见叶节点
k
,若有右子树优先入栈(此处为j
),但优先往左子树方向走(i
入栈); i
的右子树h
入栈,i
无左子树,所以继续对右子树h
进行操作;h
的右子树g
入栈,方向到左子树(b
入栈);b
的右子树a
入栈,b
无左子树。继续对a
进行操作,a
无子节点;- 到此为止,第一次入栈操作结束,此时栈中顶而下依次为
abghijk
; - 接下来弹出栈顶元素
a
,访问之; b
是a
的父节点,不用进行 入栈操作。弹出栈顶元素b
,访问之;- 接下来是
g
,非b
的父节点,执行入栈操作,按照1~5步骤说的方法,依次将fedc
入栈;
- 接下来判断是否需要执行入栈,并不断从栈中弹出节点,并访问之;
- 最后,栈为空,遍历结束。