二叉树的遍历是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序、中序、后序和层次四种遍历算法。
一、先序(NLR)遍历(PreOrder)
1. 先序遍历的概念
若二叉树为空,则什么也不做;否则:
① 访问根节点;② 先序遍历左子树;③ 先序遍历右子树。(简记为“根左右”)
可以得到下图二叉树的先序遍历序列为:ABDEC
一个将二叉树转换为先序遍历的简单高效的方法:对于先序遍历,我们可以在每个结点的左边画一个小标记,然后从根结点出发,将小标记依次连接起来,其连接的顺序就是先序遍历的访问顺序。
2. 先序遍历的递归算法代码
二叉树的链式存储结构描述如下:
typedef struct BiTNode{
ElemType data;//数据域
struct BiTNode *leftChild, *rightChild;//左右孩子指针
}BiTNode, *BiTree;
先序遍历的递归算法很简单,通过对概念的了解我们可以很容易的理解这段代码。
void PreOrder(BiTree T){//先序遍历二叉树(递归)
if(T != NULL){
visit(T);//访问根节点
PreOrder(T->leftChild);//递归遍历左子树
PreOrder(T->rightChild);//递归遍历右子树
}
}//设二叉树的结点有n个,则时间复杂度和空间复杂度都为O(n)
3. 先序遍历的非递归算法——利用栈
了解即可,非递归遍历算法的难度较大,因此考察的并不多。
void PreOrder2(BiTree T){//先序遍历二叉树(非递归)
InitStack(S);//初始化栈S
BiTree p = T;//p为用来遍历的指针
while(p || !IsEmpty(S)){//p不空或栈不空时进入循环
if(p){//一路向左
visit(p);//访问当前结点
Push(S, p);//当前结点入栈
p = p->leftChild;//若左孩子不空,则一直向左走
}
else{//出栈,并转向出栈结点的右子树
Pop(S, p);//栈顶元素出栈
p = p->rightChild;//向右子树走,p赋值为当前结点的右孩子
}//返回while循环继续进入if-else语句
}
}
4. 例题
【2015 统考真题】先序序列为 a,b,c,d 的不同二叉树的个数是()。
A. 13
B. 14
C. 15
D. 16
B(相当于问“以序列a、b、c、d为入栈次序,则出栈序列的个数为?”,利用Catalan公式)
二、中序(LNR)遍历(InOrder)
1. 中序遍历的概念
若二叉树为空,则什么也不做;否则:
① 中序遍历左子树;② 访问根节点;③ 中序遍历右子树。(简记为“左根右”)
可以得到下图二叉树的中序遍历序列为:DBEAC
一个将二叉树转换为中序遍历的简单高效的方法:对于中序遍历,我们可以在每个结点的下方画一个小标记,然后从根结点出发,遇到下方标记时,访问对应的结点,该顺序就是中序遍历的访问顺序。
2. 中序遍历的递归算法代码
中序遍历的递归算法很简单,通过对概念的了解我们可以很容易的理解这段代码。
void InOrder(BiTree T){//中序遍历二叉树(递归)
if(T != NULL){
InOrder(T->leftChild);//递归遍历左子树
visit(T);//访问根节点
InOrder(T->rightChild);//递归遍历右子树
}
}//设二叉树的结点有n个,则时间复杂度和空间复杂度都为O(n)
3. 中序遍历的非递归算法——利用栈
了解即可,非递归遍历算法的难度较大,因此考察的并不多。
借助栈来分析中序遍历的访问过程:
① 沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点;
② 栈顶元素出栈并访问:若其右孩子为空,继续执行②,若其右孩子不空,将右子树转执行①。
void InOrder2(BiTree T){//中序遍历二叉树(非递归)
InitStack(S);//初始化栈S
BiTree p = T;//p为用来遍历的指针
while(p || !IsEmpty(S)){//p不空或栈不空时进入循环
if(p){//一路向左
Push(S, p);//当前结点入栈
p = p->leftChild;//若左孩子不空,则一直向左走
}
else{//出栈,并转向出栈结点的右子树
Pop(S, p);//栈顶元素出栈
visit(p);//访问出栈结点
p = p->rightChild;//向右子树走,p赋值为当前结点的右孩子
}//返回while循环继续进入if-else语句
}
}
4. 例题
① 设 n ,m 为一棵二叉树上的两个结点,在中序遍历时,n 在 m 前的条件是( C )。
A. n 在 m 右方
B. n 是 m 祖先
C. n 在 m 左方
D. n 是 m 子孙
② 【2022 统考真题】若结点 p 与 q 在二叉树 T 的中序遍历序列中相邻,且 p 在 q 之前,则下列 p 与 q 的关系中,不可能的是( B )。
I. q 是 p 的双亲
II. q 是 p 的右孩子
III. q 是 p 的右兄弟
IV. q 是 p 的双亲的双亲
A. 仅I
B. 仅III
C. 仅II 、III
D. 仅II 、IV
三、后序(LRN)遍历(PostOrder)
1. 后序遍历的概念
若二叉树为空,则什么也不做;否则:
① 后序遍历左子树;② 后序遍历右子树;③ 访问根节点;(简记为“左右根”)
可以得到下图二叉树的后序遍历序列为:DEBCA
一个将二叉树转换为后序遍历的简单高效的方法:对于后序遍历,我们可以在每个结点的右边画一个小标记,然后从根结点出发,将小标记依次连接起来,其连接的顺序就是后序遍历的访问顺序。
2. 后序遍历的递归算法代码
后序遍历的递归算法很简单,通过对概念的了解我们可以很容易的理解这段代码。
void PostOrder(BiTree T){//后序遍历二叉树(递归)
if(T != NULL){
PostOrder(T->leftChild);//递归遍历左子树
PostOrder(T->rightChild);//递归遍历右子树
visit(T);//访问根节点
}
}//设二叉树的结点有n个,则时间复杂度和空间复杂度都为O(n)
3. 后序遍历的非递归算法——利用栈
了解即可,非递归遍历算法的难度较大,因此考察的并不多。其中先序遍历和中序遍历的基本思想是类似的,后序遍历的非递归实现是三种遍历方法中最难的,因为在后序遍历中,要保证左孩子和右孩子都已被访问且左孩子在右孩子前被访问才能访问根节点。
后序非递归遍历算法的思路是:从根节点开始,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,但是此时不能出栈并访问,因为若其有右子树,则还需按相同的规则对其右子树进行处理。直至上述操作进行不下去,若栈顶元素想要出栈被访问,要么右子树为空,要么右子树刚被访问完(此时左子树早已访问完),这样就保证了正确的访问顺序。
借助栈来分析后序遍历的访问过程:
① 沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点;
② 读栈顶元素:若其右孩子不空且未被访问过,将右子树转执行①,否则,栈顶元素出栈并访问。
【在上述思想的第②步中,必须分清返回时是从左子树返回还是从右子树返回的,因此设定一个辅助指针r,用于指向最近访问过的结点。也可以在结点中增加一个标志域,记录是否已被访问。】
void PostOrder2(BiTree T){//后序遍历二叉树(非递归)
InitStack(S);//初始化栈S
BiTNode *p = T;//p为用来遍历的指针
BiTNode *r = NULL;//r用来指向最近被访问过的结点
while(p || !IsEmpty(S)){//p不空或栈不空时进入循环
if(p){//一路向左
Push(S, p);//当前结点入栈
p = p->leftChild;//若左孩子不空,则一直向左走
}
else{//向右
GetTop(S, p);//读栈顶指针(非出栈)
if(p->rightChild && p->rightChild != r){//若右子树存在且未被访问过
p = p->rightChild;//转向右
}
else{//否则,弹出结点并访问
Pop(S, p);//将结点弹出
visit(p->data);//访问该结点
r = p;//记录最近访问过的结点
p = NULL;//结点访问完后,重置p指针
}
}
}
}//每次出栈访问完一个结点就相当于遍历完以该结点为根的子树,需将p置为NULL
实际上,在后序非递归算法遍历中访问一个结点p时,栈中结点恰好是结点p的所有祖先,从栈底到栈顶结点再加上结点p,刚好构成从根结点到结点p的一条路径。因此在求根到某结点的路径、求两个结点的最近公共祖先等问题时,可以利用这一思路来求解。
4. 例题
① 设 n ,m 为一棵二叉树上的两个结点,在后序遍历时, n 在 m 前的条件是( D )。
A . n 在 m 右方
B. n 是 m 祖先
C. n 在 m 左方
D. n 是 m 子孙
② 在二叉树中有两个结点 m 和 n, 若 m 是 n 的祖先,则使用( C )可以找到从 m 到 n 的路径。
A. 先序遍历
B. 中序遍历
C. 后序遍历
D. 层次遍历
③ 在二叉树的前序序列、中序序列和后序序列中,所有叶结点的先后顺序( B )。
A. 都不相同
B. 完全相同
C. 前序和中序相同,而与后序不同
D. 中序和后序相同,而与前序不同
④ 某二叉树采用二叉链表存储结构,若要删除这二叉链表中的所有结点,并释放它们占用的存储空间,则采用( C )遍历方法最合适。
A. 中序
B. 层次
C. 后序
D. 先序
⑤ 【2017 统考真题】某二叉树的树形如下图所示,其后序序列为 e, a, c, b, d, g, f,树中与结点 a 同层的结点是( B )。
A. c
B. d
C. f
D. g
⑥ 【2023 统考真题】已知一棵二叉树的树形如下图所示,若其后序遍历序列为 fdbeca,则其先(前)序遍历序列是( A )。
A. aedfbc
B. acebdf
C. cabefd
D. dfebac
四、层次遍历(LevelOrder)
1. 层次遍历的概念
下图为二叉树的层次遍历,即按照箭头所指的方向,按照1、2、3、4的层次顺序,自上而下、从左至右的对二叉树的各个结点进行逐层访问.
可以得到下图二叉树的层次遍历序列为:ABCDEFGHI
2. 层次遍历的算法——利用队列
进行层次遍历时需要借助一个队列,层次遍历的思想为:
① 将二叉树的根结点入队;
② 若队列非空,则队头结点出队并访问该结点,若它有左孩子,则将其左孩子入队;若它有右孩子,则将其右孩子入队;
③ 重复步骤②,直至队列为空。
void LevelOrder(BiTree T){//层次遍历二叉树
InitQueue(Q);//初始化辅助队列
BiTree p;//p为用来遍历的指针
EnQueue(Q, T);//将根结点入队
while(!IsEmpty(Q)){//若队列不空则进入循环
DeQueue(Q, p);//队头结点出队
visit(p);//访问出队结点
if(p->leftChild != NULL){//若左孩子不空,则左孩子入队
EnQueue(Q, p->leftChild);
}
if(p->rightChild != NULL){//若右孩子不空,则右孩子入队
EnQueue(Q, p->rightChild);
}
}
}
二叉树层次遍历的算法很重要,大家需要熟练掌握。
五、由遍历序列构造二叉树
对于一颗给定的二叉树,由上述的方法我们可以唯一确定它的前序序列、中序序列、后序序列以及层序序列。然而,如果只给出四种遍历序列中的任意一种,却无法唯一地确定一棵二叉树。若已知中序序列,再给出其他三种遍历序列中的任意一种,就可以唯一地确定一棵二叉树。(前序序列、后序序列和层序序列两两组合,无法唯一确定一棵二叉树)
1. 由先序序列和中序序列构造二叉树
1)常规方法
在先序序列中,第一个结点一定是二叉树的根结点,而在中序遍历中,根结点必然将中序序列分割成两个子序列,前一个子序列是根的左子树的中序序列,后一个子序列是根的右子树的中序序列。左子树的中序序列和先序序列的长度是相等的,右子树的中序序列和先序序列的长度是相等的。根据这两个子序列,可以在先序序列中找到左子树的先序序列和右子树的先序序列,如此递归地分解下去,便能唯一地确定这棵二叉树。
2)数形结合的简便方法
中序序列的规则是:左 根 右,因此如果将二叉树的所有结点下降至同一平面的话,从左到右的顺序即为中序序列,可以说中序序列决定了二叉树结点在水平方向从左至右的位置顺序;而先序序列的规则是:根 左 右,先是遍历根结点,再遍历它的左孩子和右孩子,可以看出先序序列决定了二叉树结点在垂直方向从上至下的位置顺序。二叉树结点在水平和垂直方向的位置都确定后就可以构造出唯一地一颗二叉树,因此这也是为什么由先序序列和中序序列可以唯一确定一颗二叉树。
举个例子:前序序列为ABCDEF、中序序列为BDFECA。
可知,该前序序列和中序序列确定的二叉树的后序序列为:FEDCBA,层序序列为:ABCDEF。
3)例题
① 若二叉树中结点的先序序列是…a…b…,中序序列是…b…a…,则( C )。
A. 结点 a 和结点 b 分别在某结点的左子树和右子树中
B. 结点 b 在结点 a 的右子树中
C. 结点 b 在结点 a 的左子树中
D. 结点 a 和结点 b 分别在某结点的两棵非空子树中
② 一棵二叉树的前序遍历序列为1234567, 它的中序遍历序列可能是( B )。
A. 3124567
B. 1234567
C. 4135627
D. 1463572
③ 已知一棵二叉树的先序遍历结果为ABCDEF, 中序遍历结果为CBAEDF, 则后序遍历的结果为( A )。
A. CBEFDA
B. FEDCBA
C. CBEDFA
D. 不确定
④ 【2017 统考真题】要使一棵非空二叉树的先序序列与中序序列相同,其所有非叶结点须满足的条件是( B )。
A. 只有左子树
B. 只有右子树
C. 结点的度均为1
D. 结点的度均为2
2. 由后序序列和中序序列构造二叉树
1)常规方法
后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似前序序列+中序序列的方法递归地进行分解,进而唯一地确定这棵二叉树。
2)数形结合的简便方法
中序序列的规则是:左 根 右,因此如果将二叉树的所有结点下降至同一平面的话,从左到右的顺序即为中序序列,可以说中序序列决定了二叉树结点在水平方向从左至右的位置顺序;而后序序列的规则是:左 右 根,先是遍历根结点的左孩子和右孩子,再遍历根结点,可以看出后序序列决定了二叉树结点在垂直方向从下至上的位置顺序。二叉树结点在水平和垂直方向的位置都确定后就可以构造出唯一地一颗二叉树,因此这也是为什么由后序序列和中序序列可以唯一确定一颗二叉树。
举个例子:后序序列为BDCAFGE、中序序列为ABCDEFG。
可知,该后序序列和中序序列确定的二叉树的先序序列为:EACBDGF,层序序列为:EAGCFBD。
3)例题
① 若一棵二叉树的中序序列和后序序列相同,则( B )。
A. 二叉树为空树或二叉树任一结点没有左子树
B. 二叉树为空树或二叉树任一结点没有右子树
C. 二叉树为空树或二叉树中每个结点的度为1
D. 二叉树为空树或二叉树为满二叉树
② 已知一棵二叉树的后序序列为DABEC, 中序序列为DEBAC, 则先序序列为( D )。
A. ACBED
B. DECAB
C. DEABC
D. CEDBA
3. 由层序序列和中序序列构造二叉树
1)方法
在层序遍历中,第一个结点一定是二叉树的根结点,这样就将中序序列分割成左子树的中序序列和右子树的中序序列。若存在左子树,则层序序列的第二个结点一定是左子树的根,可进一步划分左子树;若存在右子树,则层序序列中紧接着的下一个结点一定是右子树的根,可进一步划分右子树。采用这种方法继续分解,就能唯一确定这棵二叉树。
举个例子:层序序列为ABDCEFGIH、中序序列为BCAEDGHFI。
可知,该层序序列和中序序列确定的二叉树的先序序列为:ABCDEFGHI,后序序列为:CBEHGIFDA。
2)例题
已知一棵二叉树的层次序列为ABCDEF, 中序序列为BADCFE, 则先序序列为( B )。
A. ACBEDF
B. ABCDEF
C. BDFECA
D. FCEDBA
4. 由先序序列和后序序列构造二叉树
1)方法
根据上面的学习,我们知道中序序列搭配其他三种序列可以唯一的确定一棵二叉树,但事实上由先序序列和后序序列也可以构造出唯一地一颗二叉树,但这颗二叉树需要满足一个充要条件,那就是:二叉树中不存在度为1的结点。(因为我们已知先序序列的规则是:根 左 右,后序序列的规则是:左 右 根,如果某个结点只存在一个孩子,那么我们无法判断该孩子是左孩子还是右孩子,因而无法唯一地确定一棵二叉树。)
那么,如果有一颗不存在结点度为1的二叉树,并给出它的先序序列和后序序列,我们该如何构造出这颗二叉树呢?方法就是:“后序逆转,相同为子,相反为右”。因为先序序列为NLR、后序序列为LRN,得到后序序列的逆序列为NRL,因此如果某结点在先序序列和后序逆序列中所处的相对位置空间是一致的,那么该结点就是后裔结点,否则就是旁系结点。以先序序列为ABCDFGE,后序序列为BFGDECA为例进行进一步的详细说明:
① 开始的结点A为根节点。
NLR:A B C D F G E
NRL:A C E D G F B
② 结点B无论在先序序列还是后序逆序列中都位于结点A的后面,因此结点B为结点A的后裔结点。由于此二叉树不存在结点度为1的情况,在先序序列(NLR)中先出现的是左孩子、后出现的是右孩子,所以结点B为结点A的左孩子。
NLR:A B C D F G E
NRL:A C E D G F B
③ 结点C在先序序列中位于结点A、B的后面,在后序逆序列中位于结点A的后面、结点B的前面,可以推出结点C是结点A的后裔结点、是结点B的旁系结点(结点C在先序序列和后序逆序列中与结点A之间的相对位置相同,而与结点B之间的相对位置相反)。所以结点B为结点A的右孩子、结点B的右兄弟。
NLR:A B C D F G E
NRL:A C E D G F B
④ 结点D在先序序列中位于结点A、B、C的后面,在后序逆序列中位于结点A、C的后面、结点B的前面,可以推出结点D是结点A、C的后裔结点、是结点B的旁系结点(结点D在先序序列和后序逆序列中与结点A、C之间的相对位置相同,而与结点B之间的相对位置相反)。所以结点D为结点C的左孩子。
NLR:A B C D F G E
NRL:A C E D G F B
⑤ 结点F在先序序列中位于结点A、B、C、D的后面,在后序逆序列中位于结点A、C、D的后面、结点B的前面,可以推出结点F是结点A、C、D的后裔结点、是结点B的旁系结点(结点D在先序序列和后序逆序列中与结点A、C、D之间的相对位置相同,而与结点B之间的相对位置相反)。所以结点F为结点D的左孩子。
NLR:A B C D F G E
NRL:A C E D G F B
⑥ 结点G在先序序列中位于结点A、B、C、D、F的后面,在后序逆序列中位于结点A、C、D的后面、结点B、F的前面,可以推出结点G是结点A、C、D的后裔结点、是结点B、F的旁系结点(结点G在先序序列和后序逆序列中与结点A、C、D之间的相对位置相同,而与结点B、F之间的相对位置相反)。所以结点G为结点D的右孩子、结点F的右兄弟。
NLR:A B C D F G E
NRL:A C E D G F B
⑦ 结点E在先序序列中位于结点A、B、C、D、F、G的后面,在后序逆序列中位于结点A、C的后面、结点B、D、F、G的前面,可以推出结点E是结点A、C的后裔结点、是结点B、D、F、G的旁系结点(结点E在先序序列和后序逆序列中与结点A、C之间的相对位置相同,而与结点B、D、F、G之间的相对位置相反),所以结点E为结点C的右孩子、结点D的右兄弟。
NLR:A B C D F G E
NRL:A C E D G F B
2)例题
① 前序为 A,B,C, 后序为 C,B,A 的二叉树共有( D )。
A. 1 棵
B. 2 棵
C. 3 棵
D. 4 棵
② 某二叉树的先序遍历序列与后序遍历序列正好相反,则该二叉树一定是( B )。
A. 空或只有一个结点
B. 高度等于其结点数
C. 任意一个结点无左孩子
D. 任意一个结点无右孩子
【注】若某非空二叉树的先序序列NLR和后序序列LRN正好相同,即NLR=LRN成立,那么L和R应均为空。所以满足条件的二叉树只有一个根结点。
③ 一颗完全二叉树的后序遍历序列为 CDBFGEA,则其先序遍历序列是( C )。(需要注意是“完全二叉树”)
A. CBDAFEG
B. ABECDFG
C. ABCDEFG
D. 无法确定
④ 设结点 X 和 Y 是二叉树中任意的两个结点。在该二叉树的先序遍历序列中 X 在 Y 之前,而在其后序遍历序列中 X 在 Y 之后,则 X 和 Y 的关系是( C )。
A. X 是 Y 的左兄弟
B. X 是 Y 的右兄弟
C. X 是 Y 的祖先
D. X 是 Y 的后裔
(该二叉树的前序序列和后序逆序列中X都在Y之前,说明Y是X的后裔结点)
⑤ 下列序列中,不能唯一地确定一棵二叉树的是( D ) 。
A. 层次序列和中序序列
B. 先序序列和中序序列
C. 后序序列和中序序列
D. 先序序列和后序序列
⑥ 【2011 统考真题】一棵二叉树的前序遍历序列和后序遍历序列分别为1, 2, 3, 4 和 4, 3, 2, 1,该二叉树的中序遍历序列不会是( C )。
A. 1, 2, 3, 4
B. 2, 3, 4, 1
C. 3, 2, 4, 1
D. 4, 3, 2, 1
⑦ 【2012 统考真题】若一棵二叉树的前序遍历序列为 a, e, b, d, c, 后序遍历序列为 b, c, d, e, a,则根结点的孩子结点( A )。
A. 只有e
B. 有e、b
C. 有e、c
D. 无法确定
(按照“后序逆转,相同为子,相反为右”的规则进行画图,由于该二叉树不一定满足“不存在度为1的结点”的要求,因此不确定是左孩子还是右孩子的话就画一条竖线下来即可)