数据结构 · 2027 考研计算机 408

第 5 章 树与二叉树

目标院校:四川大学 / 电子科技大学 | 建议用时:概念 4 小时 + 例题练习 6 小时

本章地位:树是 408 数据结构的重中之重,平均每年贡献 13~18 分:选择题稳定考性质计算(\(n_0=n_2+1\)、完全二叉树下标)、线索与转换辨析;大题几乎每年必出——遍历序列重建二叉树、哈夫曼树 WPL 与编码、二叉链表上的递归算法设计是三大高频题型。本章所有计数结论都配「代入具体小例验证 ✓」,四种遍历、非递归中序、重建、线索化、哈夫曼构造全部给出可运行级别的 C 代码,请务必动手默写。

5.1 树的基本概念与术语

定义(递归)树(tree)是 \(n\ (n\ge 0)\) 个结点的有限集。\(n=0\) 时称为空树;任一非空树满足:① 有且仅有一个特定的称为根(root)的结点;② 其余结点可分为 \(m\ (m\gt 0)\) 个互不相交的有限集 \(T_1,T_2,\dots,T_m\),每个 \(T_i\) 本身又是一棵树,称为根的子树。递归 + 互不相交是树的两大骨架:结点属于且只属于一棵子树,因此树不能有环、不能交汇。

5.1.1 基本术语:度、深度、高度、有序性

术语表
  1. 结点的度:该结点的孩子(分支)个数;树的度:各结点度的最大值。度为 0 的结点称叶子(终端结点),度 > 0 的结点称分支结点。
  2. 双亲 / 孩子 / 兄弟 / 堂兄弟:结点的子树的根是其孩子,该结点是双亲;同双亲的结点互为兄弟;双亲在同一层的结点互为堂兄弟。祖先 / 子孙:从根到该结点路径上的所有结点(含根,不含自身,按 408 惯例)是其祖先。结点的层次:根为第 1 层,其孩子为第 2 层,依此类推。结点的深度自根向下数,结点的高度自叶向上数,都从 1 开始;树的深度(高度)= 最大层次,二者相等。
  3. 有序树 / 无序树:各子树从左到右有次序(不能交换)称有序树,否则称无序树。路径:从某结点到其子孙经过的结点序列(只能自上而下),路径上经过的边数为路径长度。
一句话记忆度看孩子数,层次从根数起;结点深度从上往下、高度从下往上,树取最大值时两者相等。树中结点间只有「上下辈分」与「同辈」关系,没有环、没有横向交叉——这也是树与图的本质区别。

5.1.2 核心性质:结点数 = 度数之和 + 1

定理任意树中,结点总数 \(n\) 等于分支(边)总数 \(B\) 加 1: \[ n = B + 1,\qquad B=\sum_{i\ge 1} i\cdot n_i = n_1+2n_2+3n_3+\cdots \] 其中 \(n_i\) 是度为 \(i\) 的结点数。推导:除根以外,每个结点有且仅有一个双亲,即每条边恰好「挂起」一个结点,故边数 = \(n-1\)。两式联立得树的万能恒等式: \[ n_0+n_1+n_2+\cdots = (n_1+2n_2+3n_3+\cdots)+1\ \Longrightarrow\ n_0=n_2+2n_3+\cdots+1=\sum_{i\ge 2}(i-1)n_i+1 \]
小例验证 ✓① 单结点树:\(n=1\),\(B=0=1-1\) ✓;② 一层「星形」:根带 3 个叶孩子,\(n=4\),度数和 \(=3=4-1\) ✓,且 \(n_3=1\Rightarrow n_0=2\times1+1=3\) ✓。考试遇到「已知各度结点数求叶子」直接套 \(n_0=\sum(i-1)n_i+1\)。
例 1 高频考点 已知各度结点数求总数

一棵树中度为 3 的结点有 3 个,度为 2 的结点有 2 个,度为 1 的结点有 1 个。求叶子结点个数与结点总数。

查看解答

由 5.1.2 推论:\(n_0=n_2+2n_3+1=2+2\times3+1=9\);结点总数 \(n=n_0+n_1+n_2+n_3=9+1+2+3=15\)。

双路验算 ✓:分支数 \(B=1\times1+2\times2+3\times3=14=n-1\) ✓;再代入恒等式 \(9+1+2+3=14+1=15\) ✓。两法一致才收笔。

练习 1 易错

判断正误:(1) 结点的度等于它的孩子个数;(2) 任何结点的深度与高度之和都等于树的深度加 1;(3) 有序树中交换根的各棵子树的次序,所得到的仍是同一棵树;(4) 度为 3 的树中至少有一个结点的度为 3。

查看答案

(1) 对:度只数「往下」的孩子,与双亲无关。

(2) 错:只有位于最长路径上的结点才有 深度 + 高度 = 树深 + 1;如深度 3 的树中第 2 层的叶,深度 + 高度 = 2 + 1 = 3 < 3 + 1。

(3) 错:有序树的定义正是子树次序有意义,交换后是不同的树(无序树才可以)。

(4) 对:树的度是最大结点度,度为 3 意味着最大值确实取到 3。

5.2 二叉树的概念与性质 高频考点

5.2.1 定义、五种形态与「度为 2 的树」之别

定义(递归)二叉树是 \(n\ (n\ge 0)\) 个结点的有限集:或为空树,或由一个根结点和两棵互不相交、分别称为左子树与右子树的二叉树组成。要点:① 每个结点至多有两棵子树(度 ≤ 2);② 子树有左右之分,次序不能颠倒;③ 即使只有一棵子树,也必须说明它是左子树还是右子树。由此得二叉树的五种基本形态:空树、仅有根、根 + 只有左子树、根 + 只有右子树、根 + 左右子树齐全。
易错:二叉树 ≠ 度为 2 的树① 二叉树可以为空,且允许所有结点度 ≤ 1(如整条右斜的链);「度为 2 的树」则必须至少有一个度为 2 的结点(至少 3 个结点);
② 二叉树结点即使只有一个孩子,也要区分左孩子 / 右孩子;度为 2 的(有序)树中,独生子结点不区分左右次序——二者是不同的概念,判断题最爱在这两点上设坑。

5.2.2 四条基本性质(第 ③ 条每年都考)

性质
  1. 第 \(i\) 层至多 \(2^{i-1}\) 个结点(\(i\ge 1\))。验证:\(i=3\) 时至多 \(1,2,4\),第 3 层恰 4 个 ✓。
  2. 深度为 \(h\) 的二叉树至多 \(2^{h}-1\) 个结点(等比求和 \(1+2+\cdots+2^{h-1}\))。验证:\(h=3\) 时至多 7 个 ✓。
  3. \(n_0=n_2+1\)(叶子数 = 度 2 结点数 + 1)。推导:\(n=n_0+n_1+n_2\),又 \(n=B+1=n_1+2n_2+1\),两式相减即得。验证:取「1 个 \(n_2\) + 2 个 \(n_0\)」的最小树——根带左右两叶,\(n_2=1\),\(n_0=2=1+1\) ✓;再取两层右斜链:\(n_2=0\),\(n_0=1=0+1\) ✓。应用时注意:只有两个未知数才可解;已知总数 \(n\) 求 \(n_0\) 必须先定 \(n_1\)(一般二叉树定不了,完全二叉树由奇偶唯一确定)。
  4. 完全二叉树按层编号的双亲 / 孩子下标关系(见 5.2.4 与图 5-1)。

5.2.3 满二叉树与完全二叉树辨析

两个概念满二叉树:深度为 \(h\) 且恰有 \(2^{h}-1\) 个结点的二叉树——每一层都达到最大结点数,不存在度为 1 的结点。完全二叉树:设深度为 \(h\),除第 \(h\) 层外其余各层都满,且第 \(h\) 层的结点从左到右连续排列(与同深度满二叉树按层编号 1~\(n\) 一一对应)。完全二叉树的三大特征:① 叶子只可能出现在最下两层;② 度为 1 的结点至多一个,且若有则它只有左孩子;③ 一旦某结点度为 0(叶),其后按层序的所有结点都是叶。
易错① 「完全二叉树中度为 1 的结点数为 0 或 1」:\(n\) 为奇数 ⇒ 每个分支结点都有两个孩子 ⇒ \(n_1=0\);\(n\) 为偶数 ⇒ 最后一个结点 \(n\) 是某个结点的左孩子 ⇒ \(n_1=1\)。验证:\(n=10\)(偶)时结点 5 只有左孩子 10,\(n_1=1\) ✓;\(n=7\)(奇)时是满树,\(n_1=0\) ✓。② 满二叉树是完全二叉树的特例,反之不成立;「完全」允许缺最下层右端的结点,但不允许中间挖洞。

5.2.4 完全二叉树的下标关系与高度公式

下标公式把 \(n\) 个结点的完全二叉树按层序存入数组下标 \(1\sim n\)(下标从 1 开始!),则对结点 \(i\):① \(i=1\) 为根,无双亲;\(i\gt 1\) 时双亲为 \(\lfloor i/2\rfloor\)。
② 若 \(2i\le n\),左孩子为 \(2i\)(否则无左孩子,此时 \(i\) 必为叶子);若 \(2i+1\le n\),右孩子为 \(2i+1\)(否则无右孩子)。
③ 结点 \(i\) 的深度为 \(\lfloor\log_2 i\rfloor+1\)。含 \(n\) 个结点的完全二叉树高度: \[ h=\Big\lfloor\log_2 n\Big\rfloor+1=\Big\lceil\log_2(n+1)\Big\rceil \]
1 2 3 4 5 6 7 8 9 10 结点 i:孩子 2i、2i+1;双亲 ⌊i/2⌋ 4:孩子 8、9 ✓ 10:双亲 ⌊10/2⌋=5 ✓ 11>10 不存在 → 5 只有左孩子(n=10 偶 → n₁=1) 高度 h=⌊log₂10⌋+1=4 ✓
图 5-1 10 个结点的完全二叉树按层编号:孩子 \(2i\)、\(2i+1\),双亲 \(\lfloor i/2\rfloor\);红边标出「5 只有左孩子 10」,故 \(n_1=1\)
例 2 真题风格 完全二叉树 699 个结点求叶子

一棵完全二叉树共有 699 个结点,求其叶子结点个数与度为 1 的结点个数。

查看解答

\(n=699\) 为奇数 ⇒ 最后一个结点有兄弟 ⇒ 每个分支结点都有两个孩子,\(n_1=0\)。联立 \(n_0+n_2=699\) 与 \(n_0=n_2+1\):\(2n_2=698\),\(n_2=349\),\(n_0=350\)。验算 ✓:\(350+349=699\);分支数 \(349\times2=698=699-1\)。答:叶子 350 个,\(n_1=0\)。

套路总结:完全二叉树求叶子——先由 \(n\) 奇偶定 \(n_1\)(奇 0 偶 1),再解 \(n_0=n_2+1\) 与 \(n_0+n_1+n_2=n\) 的二元一次方程组。

练习 2

(1) 深度为 6 的二叉树最多有多少个结点?第 6 层最多多少个?(2) 一棵完全二叉树第 4 层(最深层)恰有 8 个结点且都为叶子,求结点总数。(3) 1001 个结点的完全二叉树有几个叶子?

查看答案

(1) 最多 \(2^6-1=63\) 个;第 6 层最多 \(2^5=32\) 个(前 5 层须全满才谈得上第 6 层最多)。(2) 前 3 层全满 \(2^3-1=7\),加第 4 层 8 个:\(n=15=2^4-1\) 恰为满二叉树 ✓(第 4 层最多 8 个,占满即满树)。

(3) \(n=1001\) 奇数 ⇒ \(n_1=0\),\(n_0+n_2=1001\) 与 \(n_0=n_2+1\) 联立得 \(n_0=501\)(验算 \(501+500=1001\) ✓)。

5.3 二叉树的存储与遍历 高频考点

5.3.1 顺序存储与二叉链表

两种存储顺序存储:把结点按完全二叉树的层序编号存入数组下标 \(1\sim n\),双亲 / 孩子用 5.2.4 的公式纯靠下标计算,只适合完全二叉树——一般二叉树必须「补空洞」再存,最坏情况(每层只有一个右孩子的单支树,\(n\) 个结点、深度 \(n\))需要 \(2^{n}-1\) 个单元。验证:3 个结点的右斜链 A→B→C 占下标 1、3、7,数组须开到 8 长 ✓(空间爆炸)。链式存储(二叉链表):每个结点含数据域 + 左右指针域,通用方案。

// 二叉链表存储结构(本章所有代码基于它)

typedef struct BiTNode {
    ElemType data;                      // 数据域
    struct BiTNode *lchild, *rchild;    // 左、右孩子指针
} BiTNode, *BiTree;
空链域定理\(n\) 个结点的二叉链表共 \(2n\) 个指针域,其中 \(n-1\) 个指向孩子(边数),故恰有 \(n+1\) 个空链域。验证:3 结点右斜链共 6 个指针域,A、B 各用 1 个(指向孩子)共 2 个占用,空 \(4=n+1\) 个 ✓。这些空链域正是 5.4 线索二叉树的原料。

5.3.2 先序 / 中序 / 后序 / 层序遍历

设访问根记 N、遍历左 / 右子树记 L / R:先序(先根)NLR、中序 LNR、后序 LRN;层序按层次自上而下、同层自左向右访问。

// 三种深度优先遍历:只有 visit 的位置在移动(递归框架完全相同)

void PreOrder(BiTree t) {           // 先序 NLR
    if (t != NULL) {
        visit(t);                   // ① 第一次经过就访问
        PreOrder(t->lchild);
        PreOrder(t->rchild);
    }
}

void InOrder(BiTree t) {            // 中序 LNR
    if (t != NULL) {
        InOrder(t->lchild);
        visit(t);                   // ② 第二次经过(左子树回来时)才访问
        InOrder(t->rchild);
    }
}

void PostOrder(BiTree t) {          // 后序 LRN:把 visit 移到两次递归之后即 ③
    if (t != NULL) {
        PostOrder(t->lchild);
        PostOrder(t->rchild);
        visit(t);                   // ③ 第三次经过(离开之前)才访问
    }
}
递归本质三种 DFS 遍历走的是同一条路线(每个结点被路过三次:下行、左子树返回、右子树返回),区别只是「在第几次经过时访问」。用小树 A(B(D, E), C) 现场验证:先序 ABDEC、中序 DBEAC、后序 DEBCA、层序 ABCDE。全部时间复杂度 \(O(n)\);递归栈深 \(O(h)\),\(n\) 个结点最坏(链)\(h=n\)。

5.3.3 中序非递归(栈)与层序(队列)

// 中序非递归:沿左链一路入栈,走不动就弹栈访问,再转右子树

void InOrder2(BiTree t) {
    BiTree stk[MAXSIZE]; int top = -1;   // 顺序栈,存「待回访」结点
    BiTree p = t;
    while (p != NULL || top != -1) {
        if (p != NULL) {                 // 情形一:还有左路可走
            stk[++top] = p;              // 入栈,继续向左
            p = p->lchild;
        } else {                         // 情形二:左边走完
            p = stk[top--];              // 弹栈并访问
            visit(p);
            p = p->rchild;               // 转向右子树(可能为空)
        }
    }
}
全过程演示(\(n=5\) 小树 A(B(D,E), C))
  1. 沿左链下行:A、B、D 相继入栈,栈内 [A B D],p 走到空;
  2. D 无左子 → 弹出访问 D;D 无右子 → p 仍空;弹出访问 B;转右孩子 E:E 入栈、E 左空 → 弹出访问 E,E 右空;
  3. 弹出访问 A;转右孩子 C:C 入栈、C 左空 → 弹出访问 C,栈空且 p 空 → 结束。
输出序列 D B E A C,与递归中序一致 ✓。空间:栈深最多 \(h\),\(O(h)\)。

// 层序遍历:队列——出队一个就访问,再把它的孩子从左到右入队

void LevelOrder(BiTree t) {
    BiTree q[MAXSIZE]; int front = 0, rear = 0;   // 手写顺序队列
    if (t != NULL) q[rear++] = t;
    while (front != rear) {
        BiTree p = q[front++];                    // 出队
        visit(p);
        if (p->lchild != NULL) q[rear++] = p->lchild;
        if (p->rchild != NULL) q[rear++] = p->rchild;
    }
}
易错① 层序遍历必须显式用队列,DFS 三种遍历才用栈(或递归);判断「某算法能否得到层序」先看它用了什么辅助结构。
② 中序非递归中「弹栈访问」发生在左子树处理完毕之后,所以访问完必须转右子树而不是回左;循环条件「p 非空 或 栈非空」缺一不可(还要回来处理栈中剩余结点)。③ 后序非递归最难,408 只需掌握思路:可用「先序变体 NRL + 序列逆置」得到 LRN。

5.3.4 遍历序列重建二叉树 大题模板

唯一性定理下列组合都能唯一确定一棵二叉树:{先序 + 中序}、{后序 + 中序}、{层序 + 中序}。原因:中序按「左—根—右」排布,提供左右子树的分界;先序 / 后序 / 层序分别提供根(先序第一个、后序最后一个、层序第一个)。唯有 {先序 + 后序} 不能唯一确定。
易错:先 + 后不唯一的反例两个结点的树:A 的左孩子 B 与 A 的右孩子 B。两者的先序都是 AB、后序都是 BA,但显然是两棵不同的树——因为缺少中序就无法判断 B 在 A 的左边还是右边。推论:若补充「每个结点都有两个孩子(真满二叉树)」,先 + 后才能唯一。
例 3 高频考点 先序 + 中序重建二叉树(全步骤)

已知某二叉树的先序序列为 ABDEC,中序序列为 DBEAC。(1) 画出这棵二叉树;(2) 写出它的后序序列与层序序列。

查看解答

第一步:先序首字母 A 是整棵树的根;在中序中定位 A,其左边 DBE 全是左子树结点、右边 C 是右子树结点(左 3 个、右 1 个)。第二步:在先序中划出左子树片段 BDE(先序去掉根 A 后,前 3 个属于左子树),首字母 B 是左子树的根;在中序片段 DBE 中定位 B:左边 D 为 B 的左子树、右边 E 为 B 的右子树。第三步:D、E 片段长度为 1,直接作叶;右子树片段仅 C,作 A 的右孩子。得树 A(B(D, E), C)。

(2) 对重建出的树实地走一遍:后序 = 左右根递归 → DEBCA;层序逐层 → ABCDE。回代自检:该树先序恰为 ABDEC ✓、中序恰为 DBEAC ✓。套路总结:先序(或层序、后序)出「根」,中序出「边界」;每轮用根在中序中切两段,两段长度再回去切先序(后序)片段,递归到片段长度 0 或 1。

练习 3

已知中序序列为 DBEAC,后序序列为 DEBCA,求先序序列并说明每一步的依据。

查看答案

后序最后一个是根:A;中序切成「DBE | A | C」。左子树:后序片段 DEB 的最后一个是根 B,中序 DBE 切成「D | B | E」→ D 左、E 右;右子树只剩 C。得同一棵树 A(B(D, E), C),先序 = ABDEC ✓(与例 3 相互印证)。

5.3.5 遍历的应用:深度 / 叶子 / 交换 / 宽度

// 三道经典递归:先解决空树,再「问左右子树要答案」——大题算法题的万能骨架

int Depth(BiTree t) {                  // 求深度(高度)
    if (t == NULL) return 0;          // 空树深度为 0
    int l = Depth(t->lchild);
    int r = Depth(t->rchild);
    return (l > r ? l : r) + 1;       // 取更深的子树,加上本层
}                                     // T=O(n), S=O(h)
int LeafCount(BiTree t) {             // 统计叶子数
    if (t == NULL) return 0;
    if (t->lchild == NULL && t->rchild == NULL)
        return 1;                     // 度为 0 才算叶子
    return LeafCount(t->lchild) + LeafCount(t->rchild);
}                                     // T=O(n), S=O(h)
void SwapTree(BiTree t) {             // 交换每个结点的左右子树
    if (t != NULL) {
        BiTree tmp = t->lchild;
        t->lchild = t->rchild;
        t->rchild = tmp;
        SwapTree(t->lchild);          // 递归处理交换后的两棵子树
        SwapTree(t->rchild);
    }
}                                     // T=O(n), S=O(h)
小例验证 ✓树 A(B(D, E), C):Depth = 左子树深 2、右子树深 1,取 2 + 1 = 3 ✓;LeafCount = D + E + C = 3 ✓;交换后 A(C, B(E, D)),中序由 DBEAC 变 CAEDB(整体镜像)✓。三段代码都是「空树打底 + 分治合并」,空间开销就是递归栈 \(O(h)\)。
例 4 方法 算法设计:求二叉树的宽度

设计算法求二叉树的 宽度(结点数最多的那一层的结点数),并分析时间复杂度。

查看解答

层序遍历天然按层分组:借助队列,每轮把当前队列里的全部结点(恰为一层)整体出队,出队前记录本层个数 cnt,孩子入队构成下一层。

int Width(BiTree t) {                 // 层序 + 按层计数
    if (t == NULL) return 0;
    BiTree q[MAXSIZE]; int front = 0, rear = 0;
    q[rear++] = t;
    int width = 1;
    while (front != rear) {
        int cnt = rear - front;       // 当前队列全部结点 = 本层结点数
        if (cnt > width) width = cnt;
        for (int i = 0; i < cnt; i++) {   // 本层整体出队
            BiTree p = q[front++];
            if (p->lchild != NULL) q[rear++] = p->lchild;
            if (p->rchild != NULL) q[rear++] = p->rchild;
        }
    }
    return width;                     // T=O(n), S=O(w)(w 为宽度)
}

验证:A(B(D, E), C) 各层结点数 1、2、2 → 返回 2 ✓;满二叉树 7 结点各层 1、2、4 → 返回 4 = 2^{h-1} ✓。每个结点各进出队一次,\(T(n)=O(n)\)。

5.4 线索二叉树

5.4.1 中序线索化:利用 n+1 个空链域

动机与规则普通二叉链表有 \(n+1\) 个空链域(5.3.1),白白浪费。把它们利用起来:空左链域改存中序前驱指针、空右链域改存中序后继指针,这样的指针称为线索,加线索后的树称(中序)线索二叉树。为区分指针到底指孩子还是线索,每个结点增设两个标志域:ltag / rtag = 0 表示 child 指针指向孩子;= 1 表示是线索。

// 线索存储结构 + 中序线索化(递归框架与中序遍历完全同构)

typedef struct ThreadNode {
    ElemType data;
    struct ThreadNode *lchild, *rchild;
    int ltag, rtag;                    // 0:孩子;1:线索
} ThreadNode, *ThreadTree;

ThreadTree pre = NULL;                 // 全局:刚访问完的结点(前驱)
void InThread(ThreadTree p) {
    if (p != NULL) {
        InThread(p->lchild);           // ① 线索化左子树
        if (p->lchild == NULL) {       // ② 左空 → 指向中序前驱
            p->ltag = 1;
            p->lchild = pre;
        }
        if (pre != NULL && pre->rchild == NULL) {
            pre->rtag = 1;             // ③ 前驱右空 → 前驱的后继是我
            pre->rchild = p;
        }
        pre = p;                       // ④ 滚动更新前驱
        InThread(p->rchild);           // ⑤ 线索化右子树
    }
}
例 5 易错 给具体序列标线索

对二叉树 A(B(D, E), C)(中序序列 D B E A C)进行中序线索化:(1) 共产生几条线索?(2) 逐一写出每条线索;(3) 结点 B、A 的 ltag、rtag 各是多少?

查看解答

(1) \(n=5\),空链域 \(n+1=6\) 个,全部改为线索,共 6 条(验证:链域总数 \(2n=10\),指向孩子的 \(n-1=4\) 条:A→B、A→C、B→D、B→E;\(10-4=6\) ✓)。

(2) 按中序 DBEAC 的相邻关系:D:左线索 → NULL(首结点无前驱),右线索 → B;E:左线索 → B,右线索 → A;C:左线索 → A,右线索 → NULL(尾结点无后继)。

(3) B 的左右孩子都在 → ltag = rtag = 0;A 同理 → ltag = rtag = 0(A 的右孩子是 C,虽然 C 是叶,但 A 的右链仍是孩子指针)。易错:线索只挂在「原来为空」的链域上;结点有没有孩子与它的孩子是不是叶无关。

5.4.2 线索树上找中序后继

规则在中序线索树中找结点 \(p\) 的中序后继:① 若 \(p\to\text{rtag}=1\),右链本身就是线索,\(p\to\text{rchild}\) 即后继;② 若 \(p\to\text{rtag}=0\),后继是右子树的中序首结点——从 rchild 出发沿 lchild 一路向左走到底(ltag = 0 就继续走左)。代码上即:rtag 为 1 直取 rchild,否则 while (q->ltag == 0) q = q->lchild。
验证 ✓例 5 的线索树中:E 的 rtag = 1 → 后继直接取 rchild = A ✓(DBEAC 中 E 后确是 A);A 的 rtag = 0 → 从右孩子 C 出发,C 的 ltag = 1(C 左链是线索)走不了 → 后继 = C ✓。从中序首结点(全树最左下 D)出发反复调用 InNext,即可不递归、不用栈完成中序遍历,\(T=O(n)\)、\(S=O(1)\)。
易错① 线索树里孩子指针与线索混在同一链域,任何遍历 / 查找都必须先看 ltag、rtag,不能把线索当成孩子往下钻;
② 规则随线索种类变:找先序后继看「有无左孩子」(有则左孩子即后继),找后序后继常需双亲信息——各记各的,别把中序规则套到先 / 后序上。③ 题干若增设头结点,首结点前驱与尾结点后继指向头结点(双向环),判「线索数」时仍是 \(n+1\)。
练习 4

判断正误:(1) 中序线索树中,若结点 p 的 rtag = 0,则其中序后继是 p 的右子树中最左下的结点;(2) 只要 ltag、rtag 不参与判断,线索树的遍历与普通二叉树完全一样;(3) 借助中序线索可以不使用栈和递归实现中序遍历;(4) 先序线索树找先序后继的规则与中序完全相同。

查看答案

(1) 对:右链是孩子,后继 = 右子树中序第一个结点 = 最左下结点。(2) 错:恰好相反——必须逐结点判断标志位,否则会顺着线索「瞬移」破坏次序。

(3) 对:从最左下结点开始反复取后继,空间 O(1)。(4) 错:先序后继优先看左孩子(有左孩子则后继就是它),规则不同。

5.5 树、森林与二叉树的转换

5.5.1 孩子兄弟表示法与转换规则

孩子兄弟(二叉链表)表示法树中每个结点设两个指针:firstchild 指向它的第一个孩子,nextsibling 指向它的下一个兄弟。这一存储本身就「长得像二叉树」,于是树与二叉树可以互相转换:

// 树的孩子兄弟存储

typedef struct CSNode {
    ElemType data;
    struct CSNode *firstchild, *nextsibling;   // 长子 + 右兄弟
} CSNode, *CSTree;
转换规则(左孩子右兄弟)树 → 二叉树:每个结点的左指针挂它的第一个孩子,右指针挂它的右兄弟;兄弟间依次用右链串成一条「右链」。由于根没有兄弟,转换后二叉树的根必无右子树。二叉树 → 树:逆操作——结点的左孩子及其整条右链上的结点都变成它的孩子们。森林 → 二叉树:先把每棵树各自转换为二叉树(各根均无右子树),再把第 2、3、… 棵树的根用「右兄弟」链依次挂到前一棵树根的右链上;此时二叉树的根就可能有右子树了。
原树:A(B(E,F), C, D(G)) A B C D E F G 左孩子 · 右兄弟 转换后的二叉树 A B E C F D G 蓝实线:左孩子 红虚线:右兄弟 先序 ABEFCDG · 中序 EFBCGDA(根无右子树)
图 5-2 树 → 二叉树:B 是 A 的长子(A 左链),C、D 串在 B 的右链上;E 是 B 的长子、F 接在 E 右链。两套序列完美对应(见下表)

5.5.2 遍历对应关系表 必背

树 / 森林的遍历对应二叉树的遍历记忆与验证(图 5-2)
树的先根遍历(根 → 各子树先根)先序遍历ABEFCDG = ABEFCDG ✓
树的后根遍历(各子树后根 → 根)中序遍历EFBCGDA = EFBCGDA ✓
森林的先序遍历(各树依次先根)先序遍历森林先序 = 首树先根 + 其余树先根
森林的中序遍历(各树依次后根)中序遍历各树后根序列首尾相接
例 6 真题风格 转换后的结构特征判断

判断并说明理由:(1) 任一棵树转换成的二叉树,其根必无右子树;(2) 森林转换成的二叉树,其根可能有右子树;(3) 森林 F 的先序序列等于其转换后二叉树的先序序列。

查看解答

(1) 对:根在树中是最顶层结点,没有同层兄弟,右链(兄弟链)为空。(2) 对:森林中第 2 棵树的根被视为第 1 棵树根的「右兄弟」,挂在其右链上。小例:森林 {a, b}(两棵单结点树)→ 二叉树根 a 的右孩子是 b ✓。

(3) 对:森林先序 = 依次先根遍历各树;第 1 棵树先根对应二叉树先序的开头,其余树的根都在右链上、依次被先序访问,整体恰为二叉树先序(图 5-2 亦验证 ✓)。

套路总结:见到「树 / 森林 → 二叉树」的遍历题,先默写对应表,再按二叉树序列操作;反向题(二叉树 → 树)记住「根有右子树 ⇒ 原来是森林」。

练习 5

森林 F = {T₁: a(b, c),T₂: d}。(1) 画出 F 转换后的二叉树;(2) 写出 F 的先序、中序序列;(3) 验证它们与二叉树的先序、中序一致。

查看答案

(1) a 的左孩子为 b,b 的右链挂 c;d 作为 a 的「右兄弟」挂在 a 的右链:二叉树为 a(b(∅, c), d)。(2) 森林先序 = T₁ 先根 + T₂ 先根 = a b c d;森林中序 = T₁ 后根 + T₂ 后根 = b c a d。(3) 对 a(b(∅, c), d) 实走:先序 abcd ✓;中序(左—根—右)bcad ✓。两两吻合。

5.6 哈夫曼树与并查集 高频考点

5.6.1 WPL 与哈夫曼树的构造

WPL 定义结点的带权路径长度 = 权值 \(w\times\) 路径长度 \(l\)(该结点到根的边数)。树的带权路径长度 WPL = 所有叶结点的 \(\sum w_k l_k\)。哈夫曼树(最优二叉树):同样一组叶权,能令 WPL 最小的二叉树。
哈夫曼算法(贪心)反复执行直到森林只剩一棵树:每次取出权值最小的两棵树合并为一棵新树(新根权值 = 二者之和,两树作左右子树),新树放回森林。\(n\) 个权值(初始 \(n\) 棵单结点树)恰好合并 \(n-1\) 次。
例 7 高频考点 权集 {1, 3, 4, 7, 8} 的哈夫曼树与 WPL(全过程)

给定权集 {1, 3, 4, 7, 8}:(1) 逐步构造哈夫曼树(写出每步森林状态);(2) 计算 WPL;(3) 指出度为 1 的结点个数与结点总数。

查看解答

(1) 逐轮「取两小、并新树」:① 1+3 → 4*,森林 {4*, 4, 7, 8}(4 棵);② 4*+4 → 8*,森林 {8*, 7, 8}(3 棵);③ 7+8* → 15,森林 {8, 15}(2 棵);④ 8+15 → 根 23(1 棵),构造完成(5 → 4 → 3 → 2 → 1 棵)。

(2) 读叶子的边数深度:8 在第 2 层(1 条边)、7 在第 3 层(2 条)、4 在第 4 层(3 条)、1、3 在第 5 层(4 条):

\[ \mathrm{WPL}=8\times1+7\times2+4\times3+1\times4+3\times4=8+14+12+4+12=50 \]

交叉验算 ✓:WPL 还等于各次「合并出新结点」的权值总和 \(4+8+15+23=50\) ✓,两法一致才收笔。

(3) 度为 1 的结点 0 个(四个内部结点 23、15、8*、4* 全都左右孩子齐全);结点总数 \(2n-1=9\) ✓(内部 4 + 叶 5)。

合并全过程:5 → 4 → 3 → 2 → 1 棵 初始 {1,3,4,7,8};① 1+3=4* → {4*,4,7,8}(4 棵);② 4*+4=8* → {8*,7,8}(3 棵) ③ 7+8*=15 → {8,15}(2 棵);④ 8+15=23(1 棵,构造完成) 23 8 15 7 8* 4* 4 1 3 路径长 1 → 8×1 = 8;路径长 2 → 7×2 = 14 路径长 3 → 4×3 = 12;路径长 4 → 1×4 + 3×4 = 16 WPL = 8 + 14 + 12 + 4 + 12 = 50 (合并权总和 4+8+15+23 = 50 ✓)
图 5-3 权集 {1,3,4,7,8} 的哈夫曼构造全过程(合并次序 ①~④ 见上方步骤);红字为叶权,大权离根近、小权沉底
哈夫曼树的四条性质① 没有度为 1 的结点(每次合并必产生一对孩子);② \(n\) 个叶子的哈夫曼树共 \(2n-1\) 个结点、合并 \(n-1\) 次(内部结点 \(n-1\) 个);③ 权越大的叶子离根越近;④ 形态不唯一(并列最小权时任选),但 WPL 唯一。小例验证(\(n=2\),权 {1, 2}):1 次合并、3 个结点、\(n_1=0\)、WPL \(=1+2=3\) ✓。

5.6.2 哈夫曼编码(前缀码)

前缀码令左分支为 0、右分支为 1,从根到每个叶子的 0/1 串即该字符的哈夫曼编码。由于字符只挂在叶子,任何编码都不是其他编码的前缀(前缀码),因此编码串可无歧义地唯一译码;其平均码长 = WPL / 权总和,且在所有前缀码中最短。
例 8 真题风格 编码、译码与平均码长

对例 7 的哈夫曼树(权 {1, 3, 4, 7, 8},左 0 右 1):(1) 写出各权值的编码;(2) 证明它是前缀码;(3) 译码串 01011111010;(4) 求平均码长,并与 3 位定长码比较。

查看解答

(1) 8→0;7→10;4→111;1→1100;3→1101。(2) 五个编码两两比较,任何一串都不是另一串的开头。根源性理由:字符全挂在叶子上——若某编码是另一编码的前缀,对应结点必是另一结点的祖先,而叶子不可能是其他结点的祖先 ✓。

(3) 从左向右贪婪匹配:0→8,10→7,111→4,1101→3,0→8,译出 8, 7, 4, 3, 8,且途中任何位置都只有一种可匹配的码字(前缀码保证唯一可译)✓。(4) 平均码长 = WPL / 总权 = \(50/23\approx 2.17\) 位 / 字符;定长需 \(\lceil\log_2 5\rceil=3\) 位,平均每字符省约 0.83 位(压缩率约 28%)。验证:\(8\times1+7\times2+4\times3+1\times4+3\times4=50\) ✓ 与 (1) 的码长逐一对应。

套路总结:哈夫曼编码大题三步走——构造树 → 沿路径读码 → WPL ÷ 总权得平均码长。判断「某 0/1 码集是否前缀码」直接两两查前缀,或看「字符是否全挂叶」。

5.6.3 并查集

双亲表示的集合树并查集用「双亲表示法」把每个集合存成一棵树:数组 S[i] 存 i 的双亲下标,根的 S[i] = −1(负值亦可顺便记录集合大小 / 高度的负数)。核心操作:Find(找所属集合的根)与 Union(合并两集合)。

// 并查集:Find 与 Union

#define SIZE 100
int S[SIZE];                          // S[i]:双亲下标;根为 -1
                                      // 初始化:令每个 S[i] = -1(各自成集合)
int Find(int S[], int x) {            // 返回 x 所在集合的根
    while (S[x] >= 0)
        x = S[x];                     // 沿双亲指针一路向上
    return x;                         // T=O(树高)
}

void Union(int S[], int Root1, int Root2) {   // 两根必须是不同集合
    if (Root1 == Root2) return;       // 同一集合不能自并
    S[Root2] = Root1;                 // 把 Root2 树挂在 Root1 下
}
验证与优化一句话小例:S[1]=S[2]=0、S[4]=3、S[0]=S[3]=−1,则 Find(4)=3、Find(2)=0;Union(0, 3) 后 S[3]=0,Find(4)=0 ✓。按秩合并(Union 时矮树挂高树)+路径压缩(Find 时把沿途结点直接挂到根)两者并用,单次操作平均近 \(O(1)\),是 Kruskal(第 7 章)的效率基石。
例 9 方法 并查集操作追踪

设并查集数组 S[0..5] = {−1, 0, 1, −1, 3, 4}。(1) Find(5) 返回什么?(2) 执行 Union(0, 3) 后 S 有何变化?此时 Find(5) 返回什么?(3) 若每次 Union 都把「高树挂到矮树」下,Find 会退化为多慢?

查看解答

(1) 沿双亲上行:5 → 4 → 3,S[3] = −1 是根,返回 3(共走 2 步)。(2) S[3] 变为 0(以 3 为根的树整体挂到 0 下);Find(5) 变成 5 → 4 → 3 → 0,返回 0 ✓。(3) 树可能退化成链(如 0←1←2←…),Find 最坏 \(O(n)\)——这正是按秩合并(矮树挂高树)与路径压缩要解决的问题,两者并用平均近 \(O(1)\)。

练习 6 高频考点 哈夫曼树速算

权集 {2, 3, 6, 8}:求哈夫曼树的 WPL,并写出左 0 右 1 的编码与平均码长(提示:合并权总和与逐叶计算应一致)。

查看答案

合并 2+3=5、5+6=11、8+11=19,叶深(边数)8→1、6→2、2 与 3→3,\(\mathrm{WPL}=8+12+6+9=35\);交叉验算:合并权总和 \(5+11+19=35\) ✓。编码:8→0,6→10,2→110,3→111;平均码长 \(35/19\approx1.84\) 位 / 字符。

5.7 章末自测 真题风格

限时 60 分钟,先做后看答案。难度:★★ 基础 / ★★★ 强化 / ★★★★ 冲刺。所有计数题请养成「代小例验算」的习惯再收笔。

自测 1(选择 · ★★)

度为 4 的树中,度为 4 的结点 2 个、度为 3 的 1 个、度为 2 的 3 个,其余均为叶子,则叶子结点个数为( )
A. 7 B. 8 C. 9 D. 10

查看答案

C。\(n_0=n_2+2n_3+3n_4+1=3+2+3+1=9\)。验算:\(n=9+3+1+2=15\),度数和 \(=3\times2+1\times3+2\times4=14=n-1\) ✓。

自测 2(选择 · ★★★ 高频考点)

若某二叉树的后序遍历序列与中序遍历序列完全相同,则该二叉树( )
A. 每个结点都无左子树 B. 每个结点都无右子树 C. 一定是满二叉树 D. 一定是单结点树

查看答案

B。中序 LNR 与后序 LRN 相同 ⇔ N、R 交换无影响 ⇔ R 恒为空,即每个结点都无右子树(只有左链)。小例验证:A 的左孩子 B:中序 BA、后序 BA 相同 ✓;若是右孩子则中序 AB、后序 BA 不同。对称结论:先序 = 中序 ⇔ 都无左子树。

自测 3(选择 · ★★★)

一棵完全二叉树共有 128 个结点,则其高度与度为 1 的结点数分别为( )
A. 8、1 B. 8、0 C. 7、1 D. 7、0

查看答案

A。\(h=\lfloor\log_2 128\rfloor+1=7+1=8\)(另一公式 \(\lceil\log_2 129\rceil=8\) 一致 ✓);\(n=128\) 为偶数 ⇒ 最后结点是左孩子 ⇒ \(n_1=1\)。验算:高度 8 且 \(2^7-1=127\lt128\le 255=2^8-1\) ✓。

自测 4(选择 · ★★★)

下列序列组合中,不能唯一确定一棵二叉树的是( )
A. 先序 + 中序 B. 后序 + 中序 C. 先序 + 后序 D. 层序 + 中序

查看答案

C。反例:两个结点的树,A 的左孩子 B 与 A 的右孩子 B,先序都是 AB、后序都是 BA,但树不同——没有中序就无法判断 B 在 A 的哪一侧。

自测 5(选择 · ★★★)

在中序线索二叉树中,若某结点的右链存放的是线索,则该线索指向其( )
A. 双亲结点 B. 右孩子 C. 中序前驱 D. 中序后继

查看答案

D。rtag = 1 表示右链原为空,线索化时改存中序后继;「指向右孩子」恰恰是 rtag = 0 的含义,别混。

自测 6(选择 · ★★★)

将一棵树转换为二叉树后,原树的先根遍历序列对应二叉树的( )
A. 中序遍历 B. 先序遍历 C. 后序遍历 D. 层序遍历

查看答案

B。对应表:树先根 ↔ 二叉树先序;树后根 ↔ 二叉树中序。用图 5-2 验证:ABEFCDG = ABEFCDG ✓。

自测 7(选择 · ★★★)

关于哈夫曼树,下列说法正确的是( )
A. 可能存在度为 1 的结点 B. \(n\) 个叶子的哈夫曼树共有 \(2n\) 个结点 C. \(n\) 个叶子的哈夫曼树共有 \(2n-1\) 个结点 D. 它是 WPL 最大的二叉树

查看答案

C。合并 \(n-1\) 次新增 \(n-1\) 个内部结点,共 \(n+(n-1)=2n-1\) 个;每次合并都成对产生孩子,故 \(n_1=0\)(A 错);哈夫曼树是 WPL 最小的树(D 错)。小例:\(n=2\) 时 3 个结点 \(=2\times2-1\) ✓。

自测 8(填空 · ★★★)

权集 {2, 3, 4, 5} 构造的哈夫曼树的 WPL 为 \(\underline{\hspace{1.5cm}}\)。

查看答案

合并过程:2+3=5;4+5*=9;5+9=14。\(\mathrm{WPL}=5+9+14=\mathbf{28}\)。按叶深核对面:5 在 1 层边、4 在 2、2 与 3 在 3:\(5\times1+4\times2+2\times3+3\times3=5+8+6+9=28\) ✓。

自测 9(解答 · ★★★★ 冲刺)

已知二叉树的先序序列为 ABDGCEF,中序序列为 DGBAECF。(1) 画出该二叉树;(2) 写出后序与层序序列;(3) 判断它是否为完全二叉树。

查看解答

(1) 根 A 切中序「DGB | A | ECF」;左片先序 BDG 根为 B,中序「DG | B | ∅」→ B 无右子树,D 为 B 左子树根,中序「∅ | D | G」→ G 为 D 的右孩子;右片先序 CEF 根为 C,中序「E | C | F」→ E 左 F 右。得 A(B(D(∅, G)), C(E, F))。

(2) 后序 = 左右根递归:GDBEFCA;层序:ABCDEFG。(3) 结点按层编号 1~7 连续且无洞(7 = \(2^3-1\),恰为 3 层满二叉树),是完全二叉树(也是满二叉树)。回代自检:此树先序 ABDGCEF ✓、中序 DGBAECF ✓。

自测 10(解答 · ★★★★ 冲刺)

设计算法判断二叉链表存储的二叉树是否为完全二叉树,并用 A(B, C(D, E)) 验证。

查看解答

完全二叉树的层序逐个连续。层序遍历时把空指针也照常入队:一旦出队过空指针,之后又出现非空结点,说明编号「中间有洞」。

int IsComplete(BiTree t) {
    if (t == NULL) return 1;           // 约定空树视为完全
    BiTree q[MAXSIZE]; int front = 0, rear = 0;
    q[rear++] = t;
    int seenNull = 0;
    while (front != rear) {
        BiTree p = q[front++];
        if (p == NULL)
            seenNull = 1;              // 记录:出过一个空指针
        else {
            if (seenNull) return 0;    // 空之后再出非空 → 有洞
            q[rear++] = p->lchild;     // 空指针照样入队
            q[rear++] = p->rchild;
        }
    }
    return 1;                          // T=O(n), S=O(n)
}

验证 A(B, C(D, E)):队列出队序列 A、B、C、∅、∅、D——第 3 个空之后又出现非空 D ⇒ 返回 0,非完全 ✓(D、E 是 C 的孩子,占了编号 6、7,而编号 4、5 位置空着,正是「洞」)。再验 7 结点满树:出队 7 个非空后全空 ⇒ 1 ✓。套路总结:「完全性 = 层序无洞」,用「空指针也入队 + 打标记」一次性解决;比逐结点检查下标 \(2i\)、\(2i+1\) 是否越界的写法更不易错。

5.8 本章考点总结

考点常考题型热度核心方法
树的度数恒等式选择题★★★★结点数 = 度数和 + 1;\(n_0=\sum_{i\ge2}(i-1)n_i+1\);算完用 \(B=n-1\) 验算
二叉树性质 \(n_0=n_2+1\)选择 / 填空★★★★★ 每年必考两式相减法推导;一般树无法定 \(n_1\),完全二叉树由 \(n\) 奇偶定 \(n_1\)(奇 0 偶 1)再解方程组
完全二叉树下标与高度选择 / 填空★★★★★孩子 \(2i\)、\(2i+1\),双亲 \(\lfloor i/2\rfloor\);\(h=\lfloor\log_2 n\rfloor+1=\lceil\log_2(n+1)\rceil\);高度范围 \(2^{h-1}\sim2^{h}-1\)
四种遍历与非递归实现选择 / 大题第一步★★★★★同一路线三个访问时机;中序非递归「沿左入栈—弹栈访问—转右」;层序用队列;全部 \(O(n)\)
遍历序列重建综合应用大题★★★★★ 每年必出先序 / 后序 / 层序给根,中序切左右,递归到长度 1;先 + 后不唯一(左右孩子反例)
线索二叉树选择题★★★★空链域 \(n+1\);ltag / rtag 0 孩子 1 线索;找中序后继:rtag=1 直取,否则右子树最左下
树 / 森林 ↔ 二叉树选择 / 大题★★★★左孩子右兄弟;树根无右子树、森林根有;先根 ↔ 先序,后根 / 森林中序 ↔ 中序
哈夫曼树与编码选择 / 大题★★★★★每步取两最小合并;\(n_1=0\)、结点 \(2n-1\);WPL = Σw×边数 = 合并权总和;前缀码(字符全在叶)
并查集选择(图算法铺垫)★★★双亲数组根为 −1;Find 沿双亲上行、Union 挂根;按秩合并 + 路径压缩近 \(O(1)\)
二叉链表递归算法设计综合应用大题★★★★★「空树打底 + 向左右子树要答案」骨架:深度、叶子数、交换、宽度(层序计数)、判满 / 判完全(层序空指针标记)
下一步本章过关标准:例题全部独立重做;自测 10 题至少 8 题正确;能 5 分钟内默写——\(n_0=n_2+1\) 的推导、完全二叉树下标与高度公式、中序非递归代码、重建三步法、哈夫曼构造与 WPL 两条算法;任给一棵 7 结点二叉树能在纸上写出四种遍历序列并互检。然后进入 第 6 章 图——非线性结构的另一半主战场,图的存储、遍历与最小生成树将大量复用本章的队列、递归与并查集。