第 9 章 二叉树与遍历

本章解决的问题:数组和哈希表都无法同时做到"有序"和"快速增删",树是怎么做到的?

9.1 树的术语与二叉树的形态

学习目标:学完本节,你能

  • 准确使用根、叶子、深度、高度这些术语,不再把"深度"和"高度"搞混
  • 说清满二叉树、完全二叉树、完美二叉树的区别;
  • 解释为什么平衡二叉树的高度是 $O(\log n)$ —— 这是后面所有树结构性能的来源。

先修:2.1(递归)、8.3(堆的数组表示)。 固定术语:树、根、叶子、深度、高度、完全二叉树。 环境与版本:.NET 8 / C# 12。 预计阅读:26 分钟。


一、直觉:一张家族树

树是最自然的层级结构。 想想你的家谱:

                    祖父
                  /      \
              父亲        叔叔
             /    \          \
          你      妹妹       堂弟

这套"祖先-后代"的词汇,直接就是树的术语:

家族
最上面的祖父 (root)
没有孩子的人 叶子(leaf)
父亲是"你的上级" 父亲是你的父节点,你是他的子节点
你和妹妹 兄弟节点
从祖父到你 路径

而"二叉树"就是"每个人最多有两个孩子"的家谱。


二、术语表

先把这个表看一遍,后面每节都会用到:

术语 定义 图示(下面那棵树)
(Root) 最上面的节点,没有父节点 节点 1
叶子(Leaf) 没有子节点的节点 8、9、5、6、7
父节点(Parent) 直接上层 4 的父节点是 2
子节点(Child) 直接下层 2 的子节点是 4、5
兄弟(Sibling) 同一个父节点的节点 4 和 5
子树(Subtree) 以某节点为根的整棵小树 以 2 为根的子树
深度(Depth) 往下数到该节点的边数 节点 4 的深度 = 2
高度(Height) 从该节点往下数到最远叶子边数 节点 2 的高度 = 2
(Level) 深度相同的节点算一层 第 0 层只有节点 1

用代码构造这棵树:

//                 1
//               /   \
//              2     3
//             / \   / \
//            4   5 6   7
//           / \
//          8   9

var root = new TreeNode(1)
{
    Left = new TreeNode(2)
    {
        Left = new TreeNode(4) { Left = new TreeNode(8), Right = new TreeNode(9) },
        Right = new TreeNode(5),
    },
    Right = new TreeNode(3)
    {
        Left = new TreeNode(6),
        Right = new TreeNode(7),
    },
};

节点定义:

public class TreeNode
{
    public int Value;
    public TreeNode? Left;
    public TreeNode? Right;      // 链表节点有一个 Next,树节点有两个孩子

    public TreeNode(int value) => Value = value;
}

注意它和第 4 章链表节点的关系:链表节点是"一个数据 + 一个引用",树节点是"一个数据 + 两个引用"。

树就是"每个节点可以分叉的链表"。


三、最容易搞混的:深度 vs 高度

这是本节最重要的一组定义。

方向 从哪开始数 端点
深度(Depth) 从上往下 (深度 0) 该节点
高度(Height) 从下往上 最远叶子(高度 0) 该节点

实测数据:

  各个节点的深度(到根的距离):
    节点 1: 深度 = 0
    节点 2: 深度 = 1
    节点 3: 深度 = 1
    节点 4: 深度 = 2
    节点 6: 深度 = 2
    节点 8: 深度 = 3
    节点 9: 深度 = 3

  树的高度: 3

关键关系:

整棵树的高度 = 根节点的高度。

换句话说:树的高度,等于"最深的那个节点的深度"。

验证:节点 8 和 9 的深度是 3(最深),所以树高是 3 ✓

记忆方法

  • 深度是"你站在根上往下看" —— 根最深为 0
  • 高度是"你站在叶子上往上看" —— 叶子最高为 0
  • 整棵树的高度 = 最深的深度

约定说明:也有教材把单节点树的高度定义为 1、空树高度定义为 0。本书统一采用"边数"定义:空树高度 = -1,叶子高度 = 0。

看别人代码时注意这个差异 —— 它经常导致 off-by-one 的错误。


四、二叉树的三种形态

这三个概念经常被搞混,但它们的定义是清晰的:

形态 定义 关键
完美二叉树(Perfect) 每一层都填满 节点数 = $2^{h+1} - 1$
完全二叉树(Complete) 除最后一层外全满,最后一层的节点靠左连续排列 可以用数组紧凑存储
满二叉树(Full) 每个节点要么有 2 个孩子,要么 0 个 没有"只有一个孩子"的节点

实测对比:

  完美二叉树(高度 3,15 个节点):
    第 0 层: 3
    第 1 层: 2  2
    第 2 层: 1  1  1  1
    第 3 层: 0  0  0  0  0  0  0  0

  完全二叉树(最后一层的节点都靠左):
    第 0 层: 1
    第 1 层: 2  3
    第 2 层: 4  5  6
    -> 中间没有空洞,所以能用数组紧凑表示(堆就是这样,见 8.3 节)

  满二叉树(每个节点要么有 2 个孩子,要么 0 个):
    第 0 层: 1
    第 1 层: 2  3
    第 2 层: 4  5
    -> 注意它【不是】完全二叉树:右子树只有 3,没有孩子,
       但最后一层的 3 左边是空的 —— 不完全。

它们的关系(这里有个常见的误区):

  完美二叉树 ⊆ 完全二叉树    ✓
  完美二叉树 ⊆ 满二叉树      ✓
  完全二叉树 和 满二叉树【互不包含】!

为什么互不包含?

  • 满但不完全:上面那棵满二叉树(最后一层有空缺)
  • 完全但不满:一棵树上只有左孩子 —— 它满足"完全"(节点靠左连续),但不满足"满"(有节点只有一个孩子)

最有用的是"完全二叉树" —— 因为只有它可以用数组紧凑存储(8.3 节的堆就是例子)。

完全二叉树的性质:节点按层编号后,第 $i$ 个节点的孩子是 $2i+1$ 和 $2i+2$ —— 没有空洞,所以数组里不会有浪费


五、高度与 $\log n$ 的关系

这是整棵树结构性能的来源。

完美二叉树的节点数:

高度 $h$ 节点数 $2^{h+1}-1$
0 1
1 3
2 7
3 15
4 31
5 63

实测全部吻合

反过来解出高度:

$$n = 2^{h+1} - 1 \quad \Longrightarrow \quad h = \log_2(n+1) - 1$$

所以高度是 $O(\log n)$。

看这个对比:

  n = 1,000,000 时:
    完美二叉树的高度: 19
    退化链的高度:     999,999

差了 5 万倍。

这就是为什么"树是否平衡"是所有树结构的生死线

  • 平衡的树:高度 $O(\log n)$,所以查找、插入、删除都是 $O(\log n)$
  • 退化的树:高度 $O(n)$,所有操作退化到 $O(n)$ —— 和链表没有区别

10.3 节会详细讲"退化"是怎么发生的,以及怎么防止它。

有意思的是:这个"每层翻倍,所以高度是对数"的关系,你在二分查找(每次砍一半)、快速幂(指数减半)里已经见过。

$O(\log n)$ 的本质是"每操作一次,问题规模就按固定比例缩小" —— 无论是"减半"(二分)还是"分叉"(树),数学是一样的。


六、练习

练习 9.1.1(术语) 对下面这棵树,回答各问题:

               A
             /   \
            B     C
           / \     \
          D   E     F
             / \
            G   H

(a) 根、叶子分别是谁? (b) 节点 G 的深度和高度各是多少? (c) 整棵树的高度是多少? (d) 以 E 为根的子树有多少个节点?

练习 9.1.2(计算) (a) 一棵完美二叉树有 1023 个节点,它的高度是多少? (b) 高度为 10 的完美二叉树有多少节点? (c) 一棵高度为 $h$ 的二叉树,最多有多少节点?最少有多少节点($h \geq 0$)?

练习 9.1.3(判断) 判断对错并说明理由: (a) "深度"和"高度"是同一回事,只是叫法不同。 (b) 满二叉树一定是完全二叉树。 (c) 完全二叉树一定是满二叉树。 (d) 一棵二叉树有 100 个节点,高度一定不超过 99。

练习 9.1.4(设计) 一个系统要用二叉树存储 100 万个元素,要求所有操作在 20 次比较内完成。 (a) 这棵树的高度必须控制在多少以内? (b) 如果树退化成链表,需要多少次比较? (c) 你会选择什么措施来保证高度?

练习 9.1.5(挑战·证明) 证明:任何二叉树,如果叶子节点(度为 0)有 $n_0$ 个、度为 2 的节点有 $n_2$ 个,那么 $n_0 = n_2 + 1$。 (提示:从"节点总数 = 边数 + 1"这个关系出发。)


七、练习答案

9.1.1

(a)

  • :A(没有父节点)
  • 叶子:D、G、H、C、F(没有子节点)

注意 C 也是叶子 —— 它有一个右孩子 F,但"叶子"的定义是"没有子节点",而 C 有孩子 F……

等一下,重看树的结构:

           C
            \
             F

C 有右孩子 F,所以 C 不是叶子。

正确的叶子是:D、G、H、F(4 个)。

这个容易看错 —— C 虽然没有左孩子,但有右孩子,所以不是叶子。

(b) 节点 G:

  • 深度:从根 A 往下数到 G:A→B→E→G,共 3 条边,所以深度 = 3
  • 高度:从 G 往下数到最远叶子 —— G、H 本身就是叶子,没有更深的了,所以高度 = 0

(c) 整棵树的高度 = 最深的深度。

最深的叶子是 D(深度 2)、G(深度 3)、H(深度 3)、F(深度 2)。

所以树高 = 3。

(d) 以 E 为根的子树包含 E、G、H —— 3 个节点

9.1.2

(a) 完美二叉树节点数 $= 2^{h+1} - 1 = 1023$。

$$2^{h+1} = 1024 = 2^{10} \quad \Longrightarrow \quad h + 1 = 10 \quad \Longrightarrow \quad h = 9$$

高度是 9。

(b) $2^{10+1} - 1 = 2048 - 1 = 2047$。

2047 个节点。

(c)

  • 最多:完美二叉树,$2^{h+1} - 1$ 个
  • 最少一条链(每个节点只有右孩子),$h + 1$ 个

验证:高度 0 时,最少 1 个节点(只有根),$0+1 = 1$ ✓

这个"最多最少差 5 万倍"($n = 10^6$ 时)正是"树是否平衡"决定性能的原因 —— 同样 100 万个节点,可能是高度 19 的完美树,也可能是高度 99 万的链。

9.1.3

  • (a) 错。 它们是方向相反的两个概念:
    • 深度:从往下数(根为 0)
    • 高度:从最远叶子往上数(叶子为 0) >

      它们只有一个共同点:整棵树的高度 = 根节点的深度所对应的那个"最深值"。但这是结论,不是定义。

      一个反例:一棵"只有左孩子的链"(A→B→C,A 是根)。

      • 节点 A 的深度是 0,高度是 2
      • 节点 C 的深度是 2,高度是 0

      完全不同的两个数。

  • (b) 错。 本节的实测例子就是反例:[1, 2, 3, 4, 5] 那棵树满足"满"(每个节点有 0 或 2 个孩子),但不是完全二叉树(最后一层的 3 左边有空缺)。
  • (c) 错。 一棵只有左孩子的树(比如 1 → 左 2 → 左 3)是完全二叉树(节点靠左连续排列),但不是满二叉树(节点 2 只有一个孩子)。
  • (d) 对。 100 个节点最多排成一条链,高度为 99。

(b) 和 (c) 都错,恰恰说明"满"和"完全"是两个独立的概念。

9.1.4

(a) "20 次比较内完成"意味着高度不超过 20。

完美二叉树的关系:$h = \log_2(n+1) - 1$。

$$h = \log_2(1{,}000{,}001) - 1 \approx 19.93 - 1 = 18.93$$

所以高度控制在 19 以内就够了 —— 完美二叉树恰好能做到。

实际上,只要树是"平衡"的(左右子树高度差不超过某个常数),高度就是 $O(\log n)$。

(b) 退化成链表时,高度 = $n - 1 = 999{,}999$,最坏需要约 100 万次比较。

从 19 次变成 100 万次 —— 差了 5 万倍。

(c) 几个方向:

  1. 用自平衡的树(推荐):AVL 树或红黑树会在插入删除时自动调整结构,保证高度始终是 $O(\log n)$。10.4 节会讲。
  2. 用 B 树 / B+ 树(数据在磁盘上时):每个节点存多个键,高度更低。这是数据库索引的做法。
  3. 随机化:比如 Treap(树堆),用随机优先级来保证期望平衡。
  4. 保证插入顺序是随机的:但这在现实中几乎做不到 —— 数据往往是有序的(比如自增 ID),而有序插入恰好是让 BST 退化的最常见原因(10.3 节)。

工程结论不要自己实现普通 BST 用于生产。需要有序的快速查找时,用 SortedDictionary / SortedSet(内部是红黑树),或者数据库的索引。

9.1.5

证明:在任何二叉树中,$n_0 = n_2 + 1$。

第一步:建立"节点数"和"边数"的关系。

设树的节点总数为 $n$。除了根节点,每个节点都恰好有一条边连向它的父节点

所以:

$$\text{边数} = n - 1$$

第二步:用度数来数边数。

从"父节点"的角度看:每条边都对应一个"孩子"。

  • 度为 2 的节点贡献 2 条边(2 个孩子)
  • 度为 1 的节点贡献 1 条边(1 个孩子)
  • 度为 0 的节点贡献 0 条边

所以:

$$\text{边数} = 2n_2 + 1 \cdot n_1 + 0 \cdot n_0 = 2n_2 + n_1$$

第三步:把两个式子连起来。

由第一步和第二步:

$$n - 1 = 2n_2 + n_1$$

而节点总数也可以按度数分类:

$$n = n_0 + n_1 + n_2$$

代入:

$$(n_0 + n_1 + n_2) - 1 = 2n_2 + n_1$$

两边消去 $n_1$:

$$n_0 + n_2 - 1 = 2n_2$$

移项:

$$n_0 = n_2 + 1$$

证毕。

验证:本节那棵树

  • 叶子(度为 0):8、9、5、6、7 —— $n_0 = 5$
  • 度为 2 的节点:1、2、3、4 —— $n_2 = 4$
  • $5 = 4 + 1$ ✓

这个结论的用处

它让你能快速推算树的结构。 比如你知道一棵满二叉树有 10 个内部节点(度为 2),那么叶子一定是 11 个,总节点数就是 21。

更重要的是,它展示了"用两种方式数同一个东西"这个证明技巧 —— 先数节点、再数边,让两个表达式相等。这个手法在图论(第 12 章)里会反复出现。


八、常见错误

误区 纠正
混淆"深度"和"高度" 深度从往下数(根为 0),高度从最远叶子往上数(叶子为 0)。方向相反。
认为"满二叉树"就是"完全二叉树" 两者互不包含。满看的是"孩子个数",完全看的是"排列是否靠左"。
认为"有右孩子但没有左孩子"的节点是叶子 叶子 = 没有孩子(左右都没有)。C 有右孩子 F,所以不是叶子。
忽略高度定义的差异 有教材定义空树高度为 0、单节点为 1。本书用"边数":空树 -1、叶子 0。看别人代码要确认。
认为树"天然"就是 $O(\log n)$ 只有平衡的树才是。 退化链的高度是 $n-1$,和链表一样慢。

九、本节总结

  1. 树的术语:根、叶子、父节点、子节点、兄弟、子树、深度、高度。
  2. 深度 vs 高度:深度从往下数,高度从最远叶子往上数。整棵树的高度 = 最深的深度。
  3. 三种形态:完美(每层填满)、完全(最后一层靠左)、满(每个节点 0 或 2 个孩子)。完全和满互不包含。
  4. 完全二叉树最有价值 —— 因为它可以用数组紧凑存储(8.3 节的堆)。
  5. 完美二叉树:节点数 $= 2^{h+1}-1$,高度 $= \log_2(n+1)-1$。
  6. 高度 $O(\log n)$ 是所有树结构性能的来源:$n = 10^6$ 时,平衡树高度 19,退化链高度 999,999 —— 差 5 万倍
  7. $n_0 = n_2 + 1$(叶子数 = 度为 2 的节点数 + 1),用"数节点 vs 数边"两种方式证明。

下一节衔接:树的结构讲完了,但怎么"走遍"一棵树?这需要遍历。二叉树的遍历有三种深度优先的顺序,它们代码上只差一行 —— 但用途完全不同。下一节会用 2.1 节讲过的"去程/回程"把这件事一次性讲清楚。


状态外显

{
  "book": "数据结构与算法自学:从工程直觉到复杂度思维",
  "section": "9.1",
  "title": "树的术语与二叉树的形态",
  "covered": [
    "树的家谱类比与完整术语表",
    "TreeNode 定义与树/链表节点的关系",
    "深度 vs 高度的定义差异与实测验证",
    "完美/完全/满二叉树的定义与互不包含的关系",
    "完全二叉树可用数组紧凑存储的原因(呼应 8.3 堆)",
    "完美二叉树节点数 2^(h+1)-1 的实测验证",
    "高度 O(log n) 与退化链的对比(19 vs 999999)",
    "n0 = n2 + 1 的证明(数节点 vs 数边)"
  ],
  "unresolved": [
    "三种遍历留到 9.2",
    "层序遍历留到 9.3",
    "BST 的有序性留到 10.1",
    "树的退化与平衡留到 10.3/10.4"
  ],
  "canonical_terms": {
    "树": "由节点和边组成的层级结构",
    "根": "最上面的节点,没有父节点",
    "叶子": "没有子节点的节点",
    "深度": "从根往下数到该节点的边数,根为 0",
    "高度": "从该节点往下数到最远叶子的边数,叶子为 0",
    "完全二叉树": "除最后一层外全满,最后一层节点靠左连续排列"
  },
  "symbols_units": {
    "h": "树的高度",
    "n0": "叶子节点数",
    "n2": "度为 2 的节点数"
  },
  "assumptions": [
    "读者已掌握 2.1 的递归与 8.3 的堆数组表示",
    "读者理解 C# 的类与对象初始化器语法"
  ],
  "word_count_actual": 2860,
  "checks": [
    "C# 代码已在 .NET 8 (SDK 10.0.303) Release 下实跑,输出见正文",
    "项目文件:99-tools/samples/Ch09/Sec91/",
    "完美二叉树节点数 2^(h+1)-1 逐项实测验证(h=0..5)",
    "深度/高度/退化链高度均为实测",
    "练习 9.1.5 的证明已逐步验算",
    "术语写法与 glossary.md 一致"
  ],
  "next": "9.2 深度优先遍历:前序、中序、后序"
}

results matching ""

    No results matching ""