5.3 二叉树的遍历

先序、中序、后序、层次遍历的递归与非递归实现,以及由遍历序列构造二叉树。

考查要求 ★★★ 重点掌握 三种递归遍历非递归遍历层次遍历由遍历序列构造二叉树

什么是遍历

二叉树的遍历是指按某条搜索路径访问树中每个结点,使得每个结点均被访问一次,而且仅被访问一次。由于二叉树是一种非线性结构,每个结点都可能有两棵子树,因而需要寻找一种规律,以便使二叉树上的结点能排列在一个线性队列上。

由二叉树的递归定义可知,遍历一棵二叉树便要决定对根结点 N、左子树 L 和右子树 R 的访问顺序。按照先遍历左子树再遍历右子树的原则,常见的遍历次序有先序(NLR)、中序(LNR)和后序(LRN)三种,其中”序”指的是根结点在何时被访问。

递归遍历

动画演示先序遍历(递归)
ABDEGCFH递归工作栈(栈底 → 栈顶)A访问序列
当前结点正在访问已访问(数字为访问次序)在栈中
C 代码
1void PreOrder(BiTree T) {
2 if (T != NULL) {
3 visit(T); // 访问根结点
4 PreOrder(T->lchild); // 递归遍历左子树
5 PreOrder(T->rchild); // 递归遍历右子树
6 }
7}
1/41调用 先序(A),A 进入递归工作栈

切换上面的下拉框,分别观察先序、中序、后序遍历。注意递归工作栈的变化:它恰好记录了从根到当前结点的路径。

遍历规则示例 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

三种遍历算法中,递归遍历左、右子树的顺序都是固定的,只是访问根结点的顺序不同。不管采用哪种遍历算法,每个结点都访问一次且仅访问一次,故时间复杂度都是 O(n)O(n)。在递归遍历中,递归工作栈的栈深恰好为树的深度,所以在最坏情况下,二叉树是有 nn 个结点且深度为 nn 的单支树,空间复杂度为 O(n)O(n)。

非递归遍历

借助栈,可以将递归算法转换为非递归算法。以中序遍历为例:

  1. 沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点;
  2. 栈顶元素出栈并访问:若其右孩子为空,继续执行步骤 2;若其右孩子不空,将右子树转执行步骤 1。
动画演示中序遍历(非递归,栈)
ABDEGCFHp栈(栈底 → 栈顶)访问序列
当前结点正在访问已访问(数字为访问次序)在栈中
C 代码
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}
1/35p 指向根结点
  • 先序非递归:与中序几乎一样,只是把”访问”放在入栈之前。
  • 后序非递归最复杂:一个结点必须在其左右子树都访问完之后才能访问。做法是沿左孩子入栈;读栈顶(不出栈),若其右子树存在且未被访问过,转向右子树;否则出栈并访问。需要一个辅助指针 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 指针
            }
        }
    }
}

层次遍历

按照层序(自上而下、自左向右)遍历二叉树需要借助一个队列:先将根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根结点入队;若它有右子树,则将右子树根结点入队。然后出队,访问出队结点……如此反复,直至队列为空。

动画演示层次遍历(队列)
ABDEGCFH队列(队头 → 队尾)A访问序列
当前结点正在访问已访问(数字为访问次序)在队列中
C 代码
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}
1/26根结点 A 入队

由遍历序列构造二叉树

以先序 ABCDEFGHI、中序 BCAEDGHFI 为例:

  1. 先序的第一个结点 A 是根。在中序中找到 A,它左边的 BC 是左子树的中序,右边的 EDGHFI 是右子树的中序;
  2. 左子树有 2 个结点,对应先序中 A 之后的 BC:B 为左子树的根,中序 BC 中 C 在 B 右边,所以 C 是 B 的右孩子;
  3. 右子树的先序为 DEFGHI,根为 D;中序 EDGHFI 中 D 左边 E 为其左子树,右边 GHFI 为其右子树;
  4. 继续递归:GHFI 的先序为 FGHI,根为 F,左子树中序 GH(先序 GH,G 为根、H 为右孩子),右子树为 I。
ABCDEFGHI
由先序 ABCDEFGHI 和中序 BCAEDGHFI 构造出的二叉树
例题(408 真题 2014):求二叉树的 WPL

二叉树的带权路径长度(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);
}

时间复杂度 O(n)O(n),空间复杂度为递归栈深度 O(h)O(h)。

练习:求二叉树的高度

假设二叉树采用二叉链表存储,设计一个算法求二叉树的高度。

查看解答
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 指向下一层最右结点(即队尾)。

即时练习

即时练习5 题
点击选项即可作答
  1. 1
    在二叉树中有两个结点 m 和 n,若 m 是 n 的祖先,则使用( )可以找到从 m 到 n 的路径。
  2. 2
    若一棵二叉树的先序遍历序列为 a, e, b, d, c,后序遍历序列为 b, c, d, e, a,则根结点的孩子结点( )。
  3. 3
    先序序列为 a, b, c, d 的不同二叉树的个数是( )。
  4. 4
    某二叉树的中序序列和后序序列正好相同,则该二叉树一定是( )。
  5. 5
    对二叉树的结点从 1 开始进行连续编号,要求每个结点的编号大于其左、右孩子的编号,同一结点的左右孩子中,其左孩子的编号小于其右孩子的编号,可采用( )次序的遍历实现编号。
学完了吗?

标记完成后,侧栏会显示进度。进度仅保存在本浏览器中。