5.3 二叉树的遍历
先序、中序、后序、层次遍历的递归与非递归实现,以及由遍历序列构造二叉树。
什么是遍历
二叉树的遍历是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。由于二叉树是一种非线性结构,每个结点都可能有两棵子树,因而需要寻找一种规律,以便使二叉树上的结点能排列在一个线性队列上。
由二叉树的递归定义可知,遍历一棵二叉树便要决定对根结点 N、左子树 L 和右子树 R 的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序(NLR)、中序(LNR)和后序(LRN)三种,其中”序”指的是根结点在何时被访问。
递归遍历
1void PreOrder(BiTree T) {2 if (T != NULL) {3 visit(T); // 访问根结点4 PreOrder(T->lchild); // 递归遍历左子树5 PreOrder(T->rchild); // 递归遍历右子树6 }7}
切换上面的下拉框,分别观察先序、中序、后序遍历。注意递归工作栈的变化:它恰好记录了从根到当前结点的路径。
| 遍历 | 规则 | 示例 1 的序列 |
|---|---|---|
| 先序 NLR | 访问根 → 先序遍历左子树 → 先序遍历右子树 | A B D E G C F H |
| 中序 LNR | 中序遍历左子树 → 访问根 → 中序遍历右子树 | D B G E A C H F |
| 后序 LRN | 后序遍历左子树 → 后序遍历右子树 → 访问根 | D G E B H F C A |
三种遍历算法中,递归遍历左、右子树的顺序都是固定的,只是访问根结点的顺序不同。不管采用哪种遍历算法,每个结点都访问一次且仅访问一次,故时间复杂度都是 。在递归遍历中,递归工作栈的栈深恰好为树的深度,所以在最坏情况下,二叉树是有 个结点且深度为 的单支树,空间复杂度为 。
非递归遍历
借助栈,可以将递归算法转换为非递归算法。以中序遍历为例:
- 沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点;
- 栈顶元素出栈并访问:若其右孩子为空,继续执行步骤 2;若其右孩子不空,将右子树转执行步骤 1。
1void InOrder2(BiTree T) {2 InitStack(S); BiTree p = T;3 while (p || !IsEmpty(S)) {4 if (p) { // 一路向左5 Push(S, p);6 p = p->lchild;7 } else { // 出栈,访问,转向右子树8 Pop(S, p);9 visit(p);10 p = p->rchild;11 }12 }13}
- 先序非递归:与中序几乎一样,只是把”访问”放在入栈之前。
- 后序非递归最复杂:一个结点必须在其左右子树都访问完之后才能访问。做法是沿左孩子入栈;读栈顶(不出栈),若其右子树存在且未被访问过,转向右子树;否则出栈并访问。需要一个辅助指针
r记录最近访问过的结点。
void PostOrder2(BiTree T) {
InitStack(S);
BiTNode *p = T, *r = NULL; // r 指向最近访问过的结点
while (p || !IsEmpty(S)) {
if (p) { // 走到最左边
Push(S, p);
p = p->lchild;
} else {
GetTop(S, p); // 读栈顶结点(非出栈)
if (p->rchild && p->rchild != r)
p = p->rchild; // 右子树存在且未被访问
else {
Pop(S, p);
visit(p);
r = p; // 记录最近访问过的结点
p = NULL; // 结点访问完后,重置 p 指针
}
}
}
}
层次遍历
按照层序(自上而下、自左向右)遍历二叉树需要借助一个队列:先将根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根结点入队;若它有右子树,则将右子树根结点入队。然后出队,访问出队结点……如此反复,直至队列为空。
1void LevelOrder(BiTree T) {2 InitQueue(Q);3 BiTree p;4 EnQueue(Q, T); // 根结点入队5 while (!IsEmpty(Q)) {6 DeQueue(Q, p); // 队头结点出队7 visit(p);8 if (p->lchild != NULL)9 EnQueue(Q, p->lchild); // 左孩子入队10 if (p->rchild != NULL)11 EnQueue(Q, p->rchild); // 右孩子入队12 }13}
由遍历序列构造二叉树
以先序 ABCDEFGHI、中序 BCAEDGHFI 为例:
- 先序的第一个结点 A 是根。在中序中找到 A,它左边的
BC是左子树的中序,右边的EDGHFI是右子树的中序; - 左子树有 2 个结点,对应先序中 A 之后的
BC:B 为左子树的根,中序BC中 C 在 B 右边,所以 C 是 B 的右孩子; - 右子树的先序为
DEFGHI,根为 D;中序EDGHFI中 D 左边E为其左子树,右边GHFI为其右子树; - 继续递归:
GHFI的先序为FGHI,根为 F,左子树中序GH(先序GH,G 为根、H 为右孩子),右子树为I。
二叉树的带权路径长度(WPL)是二叉树中所有叶结点的带权路径长度之和。给定一棵二叉树 T,采用二叉链表存储,结点结构为 left | weight | right,其中叶结点的 weight 域保存该结点的非负权值。设 root 为指向 T 的根结点的指针,请设计求 T 的 WPL 的算法。
查看解答
设计思想:基于先序递归遍历,用参数 deep 记录当前结点的深度(根的深度为 0)。若当前结点是叶结点,则累加 weight × deep;否则对左右子树递归,深度加 1。
typedef struct BiTNode {
int weight;
struct BiTNode *left, *right;
} BiTNode, *BiTree;
int wpl_PreOrder(BiTree root, int deep) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL)
return root->weight * deep; // 叶结点
return wpl_PreOrder(root->left, deep + 1)
+ wpl_PreOrder(root->right, deep + 1);
}
int WPL(BiTree root) {
return wpl_PreOrder(root, 0);
}时间复杂度 ,空间复杂度为递归栈深度 。
假设二叉树采用二叉链表存储,设计一个算法求二叉树的高度。
查看解答
int Depth(BiTree T) {
if (T == NULL) return 0;
int l = Depth(T->lchild);
int r = Depth(T->rchild);
return (l > r ? l : r) + 1; // 后序:左右子树高度的较大者 + 1
}非递归做法:层次遍历,用 last 指针指向当前层最右结点,每当出队结点等于 last 时层数加 1,并让 last 指向下一层最右结点(即队尾)。
即时练习
- 1在二叉树中有两个结点 m 和 n,若 m 是 n 的祖先,则使用( )可以找到从 m 到 n 的路径。
- 2若一棵二叉树的先序遍历序列为 a, e, b, d, c,后序遍历序列为 b, c, d, e, a,则根结点的孩子结点( )。
- 3先序序列为 a, b, c, d 的不同二叉树的个数是( )。
- 4某二叉树的中序序列和后序序列正好相同,则该二叉树一定是( )。
- 5对二叉树的结点从 1 开始进行连续编号,要求每个结点的编号大于其左、右孩子的编号,同一结点的左右孩子中,其左孩子的编号小于其右孩子的编号,可采用( )次序的遍历实现编号。
标记完成后,侧栏会显示进度。进度仅保存在本浏览器中。