第 5 章 树与二叉树
本章地位:树是 408 数据结构的重中之重,平均每年贡献 13~18 分:选择题稳定考性质计算(\(n_0=n_2+1\)、完全二叉树下标)、线索与转换辨析;大题几乎每年必出——遍历序列重建二叉树、哈夫曼树 WPL 与编码、二叉链表上的递归算法设计是三大高频题型。本章所有计数结论都配「代入具体小例验证 ✓」,四种遍历、非递归中序、重建、线索化、哈夫曼构造全部给出可运行级别的 C 代码,请务必动手默写。
5.1 树的基本概念与术语
5.1.1 基本术语:度、深度、高度、有序性
- 结点的度:该结点的孩子(分支)个数;树的度:各结点度的最大值。度为 0 的结点称叶子(终端结点),度 > 0 的结点称分支结点。
- 双亲 / 孩子 / 兄弟 / 堂兄弟:结点的子树的根是其孩子,该结点是双亲;同双亲的结点互为兄弟;双亲在同一层的结点互为堂兄弟。祖先 / 子孙:从根到该结点路径上的所有结点(含根,不含自身,按 408 惯例)是其祖先。结点的层次:根为第 1 层,其孩子为第 2 层,依此类推。结点的深度自根向下数,结点的高度自叶向上数,都从 1 开始;树的深度(高度)= 最大层次,二者相等。
- 有序树 / 无序树:各子树从左到右有次序(不能交换)称有序树,否则称无序树。路径:从某结点到其子孙经过的结点序列(只能自上而下),路径上经过的边数为路径长度。
5.1.2 核心性质:结点数 = 度数之和 + 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) 结点的度等于它的孩子个数;(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 的树」之别
② 二叉树结点即使只有一个孩子,也要区分左孩子 / 右孩子;度为 2 的(有序)树中,独生子结点不区分左右次序——二者是不同的概念,判断题最爱在这两点上设坑。
5.2.2 四条基本性质(第 ③ 条每年都考)
- 第 \(i\) 层至多 \(2^{i-1}\) 个结点(\(i\ge 1\))。验证:\(i=3\) 时至多 \(1,2,4\),第 3 层恰 4 个 ✓。
- 深度为 \(h\) 的二叉树至多 \(2^{h}-1\) 个结点(等比求和 \(1+2+\cdots+2^{h-1}\))。验证:\(h=3\) 时至多 7 个 ✓。
- \(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\)(一般二叉树定不了,完全二叉树由奇偶唯一确定)。
- 完全二叉树按层编号的双亲 / 孩子下标关系(见 5.2.4 与图 5-1)。
5.2.3 满二叉树与完全二叉树辨析
5.2.4 完全二叉树的下标关系与高度公式
② 若 \(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 \]
一棵完全二叉树共有 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\) 的二元一次方程组。
(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 顺序存储与二叉链表
// 二叉链表存储结构(本章所有代码基于它)
typedef struct BiTNode {
ElemType data; // 数据域
struct BiTNode *lchild, *rchild; // 左、右孩子指针
} BiTNode, *BiTree;
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); // ③ 第三次经过(离开之前)才访问
}
}
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; // 转向右子树(可能为空)
}
}
}
- 沿左链下行:A、B、D 相继入栈,栈内 [A B D],p 走到空;
- D 无左子 → 弹出访问 D;D 无右子 → p 仍空;弹出访问 B;转右孩子 E:E 入栈、E 左空 → 弹出访问 E,E 右空;
- 弹出访问 A;转右孩子 C:C 入栈、C 左空 → 弹出访问 C,栈空且 p 空 → 结束。
// 层序遍历:队列——出队一个就访问,再把它的孩子从左到右入队
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;
}
}
② 中序非递归中「弹栈访问」发生在左子树处理完毕之后,所以访问完必须转右子树而不是回左;循环条件「p 非空 或 栈非空」缺一不可(还要回来处理栈中剩余结点)。③ 后序非递归最难,408 只需掌握思路:可用「先序变体 NRL + 序列逆置」得到 LRN。
5.3.4 遍历序列重建二叉树 大题模板
已知某二叉树的先序序列为 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。
已知中序序列为 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)
设计算法求二叉树的 宽度(结点数最多的那一层的结点数),并分析时间复杂度。
查看解答
层序遍历天然按层分组:借助队列,每轮把当前队列里的全部结点(恰为一层)整体出队,出队前记录本层个数 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 个空链域
// 线索存储结构 + 中序线索化(递归框架与中序遍历完全同构)
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); // ⑤ 线索化右子树
}
}
对二叉树 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 线索树上找中序后继
② 规则随线索种类变:找先序后继看「有无左孩子」(有则左孩子即后继),找后序后继常需双亲信息——各记各的,别把中序规则套到先 / 后序上。③ 题干若增设头结点,首结点前驱与尾结点后继指向头结点(双向环),判「线索数」时仍是 \(n+1\)。
判断正误:(1) 中序线索树中,若结点 p 的 rtag = 0,则其中序后继是 p 的右子树中最左下的结点;(2) 只要 ltag、rtag 不参与判断,线索树的遍历与普通二叉树完全一样;(3) 借助中序线索可以不使用栈和递归实现中序遍历;(4) 先序线索树找先序后继的规则与中序完全相同。
查看答案
(1) 对:右链是孩子,后继 = 右子树中序第一个结点 = 最左下结点。(2) 错:恰好相反——必须逐结点判断标志位,否则会顺着线索「瞬移」破坏次序。
(3) 对:从最左下结点开始反复取后继,空间 O(1)。(4) 错:先序后继优先看左孩子(有左孩子则后继就是它),规则不同。
5.5 树、森林与二叉树的转换
5.5.1 孩子兄弟表示法与转换规则
// 树的孩子兄弟存储
typedef struct CSNode {
ElemType data;
struct CSNode *firstchild, *nextsibling; // 长子 + 右兄弟
} CSNode, *CSTree;
5.5.2 遍历对应关系表 必背
| 树 / 森林的遍历 | 对应二叉树的遍历 | 记忆与验证(图 5-2) |
|---|---|---|
| 树的先根遍历(根 → 各子树先根) | 先序遍历 | ABEFCDG = ABEFCDG ✓ |
| 树的后根遍历(各子树后根 → 根) | 中序遍历 | EFBCGDA = EFBCGDA ✓ |
| 森林的先序遍历(各树依次先根) | 先序遍历 | 森林先序 = 首树先根 + 其余树先根 |
| 森林的中序遍历(各树依次后根) | 中序遍历 | 各树后根序列首尾相接 |
判断并说明理由:(1) 任一棵树转换成的二叉树,其根必无右子树;(2) 森林转换成的二叉树,其根可能有右子树;(3) 森林 F 的先序序列等于其转换后二叉树的先序序列。
查看解答
(1) 对:根在树中是最顶层结点,没有同层兄弟,右链(兄弟链)为空。(2) 对:森林中第 2 棵树的根被视为第 1 棵树根的「右兄弟」,挂在其右链上。小例:森林 {a, b}(两棵单结点树)→ 二叉树根 a 的右孩子是 b ✓。
(3) 对:森林先序 = 依次先根遍历各树;第 1 棵树先根对应二叉树先序的开头,其余树的根都在右链上、依次被先序访问,整体恰为二叉树先序(图 5-2 亦验证 ✓)。
套路总结:见到「树 / 森林 → 二叉树」的遍历题,先默写对应表,再按二叉树序列操作;反向题(二叉树 → 树)记住「根有右子树 ⇒ 原来是森林」。
森林 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 与哈夫曼树的构造
给定权集 {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.6.2 哈夫曼编码(前缀码)
对例 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 并查集
// 并查集: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[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)\)。
权集 {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 分钟,先做后看答案。难度:★★ 基础 / ★★★ 强化 / ★★★★ 冲刺。所有计数题请养成「代小例验算」的习惯再收笔。
度为 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\) ✓。
若某二叉树的后序遍历序列与中序遍历序列完全相同,则该二叉树( )
A. 每个结点都无左子树 B. 每个结点都无右子树 C. 一定是满二叉树 D. 一定是单结点树
查看答案
B。中序 LNR 与后序 LRN 相同 ⇔ N、R 交换无影响 ⇔ R 恒为空,即每个结点都无右子树(只有左链)。小例验证:A 的左孩子 B:中序 BA、后序 BA 相同 ✓;若是右孩子则中序 AB、后序 BA 不同。对称结论:先序 = 中序 ⇔ 都无左子树。
一棵完全二叉树共有 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\) ✓。
下列序列组合中,不能唯一确定一棵二叉树的是( )
A. 先序 + 中序 B. 后序 + 中序 C. 先序 + 后序 D. 层序 + 中序
查看答案
C。反例:两个结点的树,A 的左孩子 B 与 A 的右孩子 B,先序都是 AB、后序都是 BA,但树不同——没有中序就无法判断 B 在 A 的哪一侧。
在中序线索二叉树中,若某结点的右链存放的是线索,则该线索指向其( )
A. 双亲结点 B. 右孩子 C. 中序前驱 D. 中序后继
查看答案
D。rtag = 1 表示右链原为空,线索化时改存中序后继;「指向右孩子」恰恰是 rtag = 0 的含义,别混。
将一棵树转换为二叉树后,原树的先根遍历序列对应二叉树的( )
A. 中序遍历 B. 先序遍历 C. 后序遍历 D. 层序遍历
查看答案
B。对应表:树先根 ↔ 二叉树先序;树后根 ↔ 二叉树中序。用图 5-2 验证:ABEFCDG = ABEFCDG ✓。
关于哈夫曼树,下列说法正确的是( )
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\) ✓。
权集 {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\) ✓。
已知二叉树的先序序列为 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 ✓。
设计算法判断二叉链表存储的二叉树是否为完全二叉树,并用 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)\) |
| 二叉链表递归算法设计 | 综合应用大题 | ★★★★★ | 「空树打底 + 向左右子树要答案」骨架:深度、叶子数、交换、宽度(层序计数)、判满 / 判完全(层序空指针标记) |