建议先阅读: 容器概览, (递归与栈的关系),队列(层序遍历依赖队列)


原理

树(Tree)是计算机科学中最丰富的非线性结构。它是递归定义的自然体现——一棵树的子树仍然是树。这种自相似性使得递归成为树操作的首选范式。

树在哪里

文件系统是一棵倒挂的树——根目录 / 在顶部,子目录层层展开。编译器把源代码解析成抽象语法树(AST),每个表达式是一棵子树。HTML 的 DOM 是一棵树——<html> 是根,每个标签是子节点,CSS 选择器沿着树路径匹配。数据库的 B+ 树索引让百万行查询从全表扫描的 降到 。这些场景的共同主题——层次关系的自然表达与高效检索——正是树的核心用武之地。

术语体系


graph TD
    ROOT["Root 深度=0"] --> A["A 深度=1"]
    ROOT --> B["B 深度=1"]
    A --> C["C d=2"]
    A --> D["D d=2"]
    B --> E["E d=2"]
    C --> F["F d=3"]
    C --> G["G d=3"]
    style ROOT fill:#4a90d9,color:#fff
    style A fill:#5ba3e6,color:#fff
    style B fill:#5ba3e6,color:#fff
    style C fill:#7ec8e3,color:#333
    style D fill:#7ec8e3,color:#333
    style E fill:#7ec8e3,color:#333
    style F fill:#afa,color:#333
    style G fill:#afa,color:#333

节点 F 的高度 = 0 (叶子), 节点 A 的高度 = 2, 树的高度 = 3
节点 F 的深度 = 3 (从根起的边数)

术语定义例(以 F 为参考)
深度(Depth)从根到该节点的边数3
高度(Height)该节点到最深叶子的边数0(叶子)
层(Level)深度 + 1第 4 层
平衡因子(BF)左子树高度 - 右子树高度

二叉树的四种遍历

二叉树遍历是递归应用的经典场景。每种遍历将”处理当前节点”的动作放在递归调用的不同位置:

graph TD
 subgraph "某棵子树"
 ROOT["根"] --> L["左子树"]
 ROOT --> R["右子树"]
 end
 subgraph "前序: 根→左→右"
 P1["1. 访问根"] --> P2["2. 递归遍历左子树"] --> P3["3. 递归遍历右子树"]
 end
 subgraph "中序: 左→根→右"
 I1["1. 递归遍历左子树"] --> I2["2. 访问根"] --> I3["3. 递归遍历右子树"]
 end
 subgraph "后序: 左→右→根"
 O1["1. 递归遍历左子树"] --> O2["2. 递归遍历右子树"] --> O3["3. 访问根"]
 end
遍历方式顺序典型应用
前序(Pre-order)根 → 左 → 右序列化/反序列化([根, 左子树序列, 右子树序列] 唯一确定一棵树)、前缀表达式
中序(In-order)左 → 根 → 右BST 的中序遍历输出升序序列
后序(Post-order)左 → 右 → 根释放树的内存(必须先释放子树再释放根)、后缀表达式、计算目录总大小
层序(Level-order)逐层、从左到右BFS 搜索、判断完全二叉树、二叉树的最大宽度

遍历手算轨迹

以这棵完全二叉树为例:

graph TD
    N1["1"] --> N2["2"]
    N1 --> N3["3"]
    N2 --> N4["4"]
    N2 --> N5["5"]
    N3 --> N6["6"]
    N3 --> N7["7"]
遍历序列过程
前序1, 2, 4, 5, 3, 6, 7根→左→右:先访问 1,递归左子树(2→4→5),递归右子树(3→6→7)
中序4, 2, 5, 1, 6, 3, 7左→根→右:最左 4,回退 2,右 5,回退 1,右子树同理
后序4, 5, 2, 6, 7, 3, 1左→右→根:叶子先输出,根最后
层序1, 2, 3, 4, 5, 6, 7逐层从左到右:BFS 遍历

自测:遍历序列

给定二叉树:

graph TD
    A["A"] --> B["B"]
    A --> C["C"]
    B --> D["D"]
    B --> E["E"]
    C --> F["F"]

写出前序、中序、后序、层序遍历序列。

答案:

前序:A, B, D, E, C, F
中序:D, B, E, A, C, F
后序:D, E, B, F, C, A
层序:A, B, C, D, E, F

前序+中序唯一确定二叉树:给定前序和中序遍历序列,可以重建一棵二叉树。前序的第一个元素是根,在中序中找出根的位置后,左边是左子树的中序序列,右边是右子树的中序序列。递归即可构建。这是分治思想的典型应用——每个子问题是一个子树的构建。

前序+中序重建手算

给定前序 [A,B,D,E,C,F] 和中序 [D,B,E,A,F,C],重建二叉树:

第 1 步:前序首元素 A 是根。在中序中 A 的位置 → 左子树中序 [D,B,E],右子树中序 [F,C]

第 2 步:左子树的前序为 [B,D,E],首元素 B 是左子树的根。中序 [D,B,E] 中 B 的位置 → B 的左子树中序 [D],右子树中序 [E]

第 3 步:D 和 E 都是叶子。

第 4 步:右子树的前序为 [C,F],首元素 C 是右子树的根。中序 [F,C] 中 C 的位置 → C 的左子树中序 [F]

第 5 步:F 是叶子。

graph TD
    A["A"] --> B["B"]
    A --> C["C"]
    B --> D["D"]
    B --> E["E"]
    C --> F["F"]

自测:前序 [1,2,4,5,3,6,7] + 中序 [4,2,5,1,6,3,7],画出二叉树。

答案:即正文遍历手算轨迹中的完全二叉树——根 1,左子树(2,4,5),右子树(3,6,7)。

二叉搜索树(BST)

BST 通过有序性约束将查找从 加速到 (平衡情况下):

性质:对于任意节点 X,
 左子树中的所有节点值 < X 的值 < 右子树中的所有节点值
graph TD
 ROOT["8"] --> L["3"]
 ROOT --> R["10"]
 L --> LL["1"]
 L --> LR["6"]
 LR --> LRL["4"]
 LR --> LRR["7"]
 R --> RR["14"]
 RR --> RRL["13"]

中序遍历这棵 BST:1 → 3 → 4 → 6 → 7 → 8 → 10 → 13 → 14。即升序输出。

BST 的三个操作

操作平均最坏核心步骤
查找(search)与根比较:小则走左,大则走右
插入(insert)查找到失败位置,在 leaf 的空位上插入
删除(delete)三种情况:叶子(直删)、单子(用子代替)、双子(用后继代替)

BST 删除的三个情况

graph TD
 subgraph "情况1: 删除叶子节点"
 LEAF["5 → 删除 5"] --> HEALTHY["直接删除, 父节点的 left/right 置 NULL"]
 end
 subgraph "情况2: 单子节点"
 ONE["删除 3, 它有子节点 4"] --> REPLACE["用节点 4 代替节点 3 在父节点中的位置"]
 end
 subgraph "情况3: 双子节点"
 TWO["删除 8, 它有左/右子树"] --> FIND["找到中序后继(右子树的最小值)= 10"]
 FIND --> COPY["将 10 的值复制到 8 的位置"]
 COPY --> DEL10["删除值 10 的原节点(情况1或2)"]
 end

中序后继是 BST 中 key 排序下”正好比当前节点大的下一个节点”。选择它替代被删节点,可以保证 BST 性质完全保持不变——所有左子树的值 < 后继的值 < 所有右子树的值。

BST 删除手算轨迹

以 BST [8, 3, 10, 1, 6, 14, 4, 7, 13] 为例,逐步删除:

初始树

graph TD
    N8["8"] --> N3["3"]
    N8 --> N10["10"]
    N3 --> N1["1"]
    N3 --> N6["6"]
    N10 --> N14["14"]
    N6 --> N4["4"]
    N6 --> N7["7"]
    N14 --> N13["13"]

删除 7(情况 1:叶子):直接删除,父节点 6 的 right 置 NULL。

graph TD
    N8["8"] --> N3["3"]
    N8 --> N10["10"]
    N3 --> N1["1"]
    N3 --> N6["6"]
    N10 --> N14["14"]
    N6 --> N4["4"]
    N14 --> N13["13"]

删除 6(情况 2:单子):6 只有左子 4,用 4 代替 6 的位置。

graph TD
    N8["8"] --> N3["3"]
    N8 --> N10["10"]
    N3 --> N1["1"]
    N3 --> N4["4"]
    N10 --> N14["14"]
    N14 --> N13["13"]

删除 3(情况 3:双子):3 有左子 1 和右子 4。找中序后继——右子树最小值 = 4。将 4 复制到 3 的位置,删除原 4 节点(叶子)。

graph TD
    N8["8"] --> N4["4"]
    N8 --> N10["10"]
    N4 --> N1["1"]
    N10 --> N14["14"]
    N14 --> N13["13"]

自测:BST 删除

给定 BST [15, 6, 18, 3, 7, 17, 20, 2, 4, 13, 9],删除 6 后画出新树(写出所用情况)。

答案:

6 有左子(3)和右子(7) → 情况 3。中序后继 = 右子树最小值 = 7。
用 7 替换 6,删除原 7(叶子)。

graph TD
    N15["15"] --> N7["7"]
    N15 --> N18["18"]
    N7 --> N3["3"]
    N18 --> N17["17"]
    N18 --> N20["20"]
    N3 --> N2["2"]
    N3 --> N4["4"]

右子树:13 和 9 仍在 15 的左子树中——等一下,需要重新检查原始树的结构。

更正:原始 BST 中 13 在 18 的左子树路径上(15→18→17→13 不对)。按 BST 性质,13 < 15 应在 15 的左子树。实际原始树为:15(根), 左子树(6→3→(2,4), 7→(…)), 右子树(18→17, 20)。13 应在 15 的左子树中——它大于 6 小于 15,所以在 6 的右子树中。6 的右子树有 7 和 13。13 > 7 所以在 7 的右子树。删除 6 后 7 替上来,13 仍在 7 的右子树:

graph TD
    N15["15"] --> N7["7"]
    N15 --> N18["18"]
    N7 --> N3["3"]
    N7 --> N13["13"]
    N18 --> N17["17"]
    N18 --> N20["20"]
    N3 --> N2["2"]
    N3 --> N4["4"]

情况 3(双子):用中序后继 7 替换 6。

BST 退化的致命性

按递增顺序插入 个元素到空 BST:

  • 第一个元素成为根
  • 每个后续元素都比之前的所有元素大
  • 因此每个新元素都是在”最右叶子”上追加

结果是一棵深度为 的单链——退化为链表。此时查找从 变为 。这正是自平衡二叉搜索树存在的根本原因。

AVL 树——通过旋转维持平衡

AVL 树是最早的自平衡 BST(Adelson-Velsky & Landis, 1962)。核心约束:任意节点的左右子树高度差

平衡因子(BF)

当插入或删除导致某个节点的 时,需要通过旋转恢复平衡。

四种旋转场景

graph TD
 subgraph "LL — 右旋"
 LL_BEFORE["节点 X (BF=2)<br/> /<br/> Y (BF=1)<br/> /<br/>Z<br/>失衡: 左子树的左子树太重"]
 LL_BEFORE -->|"右旋(X)"| LL_AFTER["Y<br/>/<br/>Z X<br/>平衡恢复"]
 end
 subgraph "RR — 左旋"
 RR_BEFORE["节点 X (BF=-2)<br/> \\<br/> Y (BF=-1)<br/> \\<br/> Z<br/>失衡: 右子树的右子树太重"]
 RR_BEFORE -->|"左旋(X)"| RR_AFTER["Y<br/>/<br/>X Z<br/>平衡恢复"]
 end
 subgraph "LR — 先左旋后右旋"
 LR_BEFORE["节点 X (BF=2)<br/> /<br/> Y (BF=-1)<br/> \\<br/> Z<br/>失衡: 左子树的右子树太重"]
 LR_BEFORE -->|"左旋(Y) → LL"| LR_MID["X (BF=2)<br/> /<br/> Z<br/> /<br/>Y<br/>变成 LL 情形"]
 LR_MID -->|"右旋(X)"| LR_AFTER["Z<br/>/<br/>Y X<br/>平衡恢复"]
 end
 subgraph "RL — 先右旋后左旋"
 RL_BEFORE["节点 X (BF=-2)<br/> \\<br/> Y (BF=1)<br/> /<br/> Z<br/>失衡: 右子树的左子树太重"]
 RL_BEFORE -->|"右旋(Y) → RR"| RL_MID["X (BF=-2)<br/> \\<br/> Z<br/> \\<br/> Y<br/>变成 RR 情形"]
 RL_MID -->|"左旋(X)"| RL_AFTER["Z<br/>/<br/>X Y<br/>平衡恢复"]
 end

旋转的原理——以 LL 右旋为例:将失衡节点的左子节点”提上来”成为新的子树根,原根成为新根的右子节点。旋转保持了 BST 性质(中序遍历不变),同时将左子树的高度降低了 1,右子树的高度增加了 1——左右高度从不平衡的 2 恢复到 0 或 1。

AVL 建树手算轨迹

给定插入序列 [3, 2, 1, 4, 5, 6, 7],逐步构建 AVL 树并标注旋转类型:

插入树状态(简化)失衡节点旋转结果
13(3)BF(3)=0
22(3,(2))BF(3)=-1
31(3,(2,(1)))3LL右旋 → (2,(1),(3))
44(2,(1),(3,(4)))BF(3)=-1, BF(2)=-1
55(2,(1),(3,(4,(5))))3RR左旋 → (2,(1),(4,(3),(5)))
66(2,(1),(4,(3),(5,(6))))4,2RR x2先左旋4 → (2,(1),(5,(3),(4),(6)));BF(2)=-2 再左旋2 → (5,(2,(1),(4)),(6))
77(5,(2,(1),(4)),(6,(7)))6RR左旋 → (5,(2,(1),(4)),(7,(6))),检查 5 的 BF:左高=2,右高=1 → BF=1 OK

关键判断:每插入一个节点后,沿路径回溯更新高度,遇到 时根据 BF 值和新插入值的方向判定旋转类型:

  • BF=2 且新值 < 左子值 → LL(右旋)
  • BF=-2 且新值 > 右子值 → RR(左旋)
  • BF=2 且新值 > 左子值 → LR(先左旋后右旋)
  • BF=-2 且新值 < 右子值 → RL(先右旋后左旋)

自测:AVL 建树

插入序列 [10, 20, 30, 25, 28, 27],画出每次插入后的 AVL 树并标注旋转类型。

答案:

① 插入 10:(10) — BF=0

② 插入 20:(10,(20)) — BF(10)=-1

③ 插入 30:(10,(20,(30))) — BF(10)=-2,RR → 左旋 → (20,(10),(30))

④ 插入 25:(20,(10),(30,(25))) — BF(30)=-1, BF(20)=-1

⑤ 插入 28:(20,(10),(30,(25,(28)))) — BF(30)=-2,RR → 左旋 → (20,(10),(28,(25),(30))),BF(20)=-1 OK

⑥ 插入 27:(20,(10),(28,(25,(27)),(30))) — BF(28)=1, BF(20)=-1 → OK 无需旋转

旋转汇总:第 3 步 LL 一次,第 5 步 RR 一次,第 6 步 RR 两次,第 7 步 RR 一次,共 4 次旋转

AVL 高度上界的推导

为高度为 的 AVL 树中的最小节点数。最”瘦”的 AVL 树是斐波那契树——左右子树高度差恰好为 1:

这与斐波那契数列 有相同的递推关系,因此 ,其中 (黄金比)。由此反推:

即 AVL 树的高度不超过 ,保证查找、插入、删除均为 。AVL 是最严格的平衡二叉搜索树——代价是插入/删除时可能需要 次旋转(向根方向传播)。


实现

BST

#include <stdlib.h>
 
typedef struct BSTNode {
 int data;
 struct BSTNode* left;
 struct BSTNode* right;
} BSTNode;
 
BSTNode* bst_insert(BSTNode* root, int value) {
 if (!root) {
 BSTNode* node = malloc(sizeof(BSTNode));
 node->data = value;
 node->left = node->right = NULL;
 return node;
 }
 if (value < root->data)
 root->left = bst_insert(root->left, value);
 else if (value > root->data)
 root->right = bst_insert(root->right, value);
 return root;
}
 
BSTNode* bst_search(BSTNode* root, int value) {
 if (!root || root->data == value) return root;
 if (value < root->data) return bst_search(root->left, value);
 return bst_search(root->right, value);
}
 
// 删除节点: 返回新的子树根
BSTNode* bst_delete(BSTNode* root, int value) {
 if (!root) return NULL;
 if (value < root->data)
 root->left = bst_delete(root->left, value);
 else if (value > root->data)
 root->right = bst_delete(root->right, value);
 else {
 // 情况1/2: 叶子或单子节点
 if (!root->left) { BSTNode* tmp = root->right; free(root); return tmp; }
 if (!root->right) { BSTNode* tmp = root->left; free(root); return tmp; }
 // 情况3: 双子节点 — 找中序后继
 BSTNode* succ = root->right;
 while (succ->left) succ = succ->left;
 root->data = succ->data; // 替换值
 root->right = bst_delete(root->right, succ->data); // 删除后继
 }
 return root;
}
 
void bst_destroy(BSTNode* root) {
 if (!root) return;
 bst_destroy(root->left);
 bst_destroy(root->right);
 free(root);
}
 
// 树的三种递归遍历
void preorder(BSTNode* root) {
 if (!root) return;
 printf("%d ", root->data);
 preorder(root->left);
 preorder(root->right);
}
 
void inorder(BSTNode* root) {
 if (!root) return;
 inorder(root->left);
 printf("%d ", root->data);
 inorder(root->right);
}
 
void postorder(BSTNode* root) {
 if (!root) return;
 postorder(root->left);
 postorder(root->right);
 printf("%d ", root->data);
}
 
// 层序遍历 (BFS): 依赖队列
void level_order(BSTNode* root) {
 if (!root) return;
 BSTNode** q = malloc(sizeof(BSTNode*) * 1024);
 int front = 0, back = 0;
 q[back++] = root;
 while (front < back) {
  BSTNode* n = q[front++];
  printf("%d ", n->data);
  if (n->left) q[back++] = n->left;
  if (n->right) q[back++] = n->right;
 }
 free(q);
}

AVL 树

typedef struct AVLNode {
 int data;
 int height; // 该节点子树的高度 (叶子=0)
 struct AVLNode* left;
 struct AVLNode* right;
} AVLNode;
 
static int height(AVLNode* n) { return n ? n->height : -1; }
static int max(int a, int b) { return a > b ? a : b; }
static int bf(AVLNode* n) { return height(n->left) - height(n->right); }
 
// 右旋
static AVLNode* rotate_right(AVLNode* y) {
 AVLNode* x = y->left;
 AVLNode* T2 = x->right;
 x->right = y;
 y->left = T2;
 y->height = max(height(y->left), height(y->right)) + 1;
 x->height = max(height(x->left), height(x->right)) + 1;
 return x; // 新根
}
 
// 左旋
static AVLNode* rotate_left(AVLNode* x) {
 AVLNode* y = x->right;
 AVLNode* T2 = y->left;
 y->left = x;
 x->right = T2;
 x->height = max(height(x->left), height(x->right)) + 1;
 y->height = max(height(y->left), height(y->right)) + 1;
 return y;
}
 
AVLNode* avl_insert(AVLNode* root, int value) {
 if (!root) {
 AVLNode* node = malloc(sizeof(AVLNode));
 node->data = value; node->height = 0;
 node->left = node->right = NULL;
 return node;
 }
 if (value < root->data)
 root->left = avl_insert(root->left, value);
 else if (value > root->data)
 root->right = avl_insert(root->right, value);
 else
 return root; // 重复值不插入
 
 root->height = max(height(root->left), height(root->right)) + 1;
 int balance = bf(root);
 
 // LL: 左子树的左子树过重
 if (balance > 1 && value < root->left->data)
 return rotate_right(root);
 // RR: 右子树的右子树过重
 if (balance < -1 && value > root->right->data)
 return rotate_left(root);
 // LR: 左子树的右子树过重
 if (balance > 1 && value > root->left->data) {
 root->left = rotate_left(root->left);
 return rotate_right(root);
 }
 // RL: 右子树的左子树过重
 if (balance < -1 && value < root->right->data) {
 root->right = rotate_right(root->right);
 return rotate_left(root);
 }
 return root;
}

AVL 删除

删除比插入复杂——删除后沿路径回溯更新高度时,失衡可能出现在任意祖先节点,且旋转类型需根据左右子树的高度判断(而非插入时的值比较)。

AVLNode* avl_delete(AVLNode* root, int value) {
 if (!root) return NULL;
 if (value < root->data)
  root->left = avl_delete(root->left, value);
 else if (value > root->data)
  root->right = avl_delete(root->right, value);
 else {
  // 找到目标节点
  if (!root->left || !root->right) {
   AVLNode* tmp = root->left ? root->left : root->right;
   free(root);
   return tmp;
  }
  // 双子节点: 用中序后继替换
  AVLNode* succ = root->right;
  while (succ->left) succ = succ->left;
  root->data = succ->data;
  root->right = avl_delete(root->right, succ->data);
 }
 
 root->height = max(height(root->left), height(root->right)) + 1;
 int balance = bf(root);
 
 // LL: 左子树过高
 if (balance > 1 && bf(root->left) >= 0)
  return rotate_right(root);
 // LR: 左子树的右子树过高
 if (balance > 1 && bf(root->left) < 0) {
  root->left = rotate_left(root->left);
  return rotate_right(root);
 }
 // RR: 右子树过高
 if (balance < -1 && bf(root->right) <= 0)
  return rotate_left(root);
 // RL: 右子树的左子树过高
 if (balance < -1 && bf(root->right) > 0) {
  root->right = rotate_right(root->right);
  return rotate_left(root);
 }
 return root;
}

与插入的关键区别:插入时只需检查一次旋转(因为插入只影响一条路径上的一个节点)。删除后回溯过程中,每个祖先都可能失衡,且旋转后子树高度可能继续变化,因此旋转判断条件不同——插入用 value 与子节点值比较,删除用 bf() 直接判断子树平衡因子。

哈夫曼树构建

从频率数组构建哈夫曼树——每次取两个最小频率节点合并为新节点,直到剩余一个根节点。使用最小堆加速选取过程。

typedef struct HuffNode {
 int freq;
 struct HuffNode *left, *right;
} HuffNode;
 
// 用最小堆构建哈夫曼树
// freq: 字符频率数组, n: 字符数
HuffNode* huffman_build(int* freq, int n) {
 if (n <= 0) return NULL;
 
 // 构建叶子节点最小堆
 HuffNode** heap = malloc(sizeof(HuffNode*) * n);
 int heap_size = 0;
 for (int i = 0; i < n; i++) {
  if (freq[i] <= 0) continue;
  HuffNode* node = malloc(sizeof(HuffNode));
  node->freq = freq[i];
  node->left = node->right = NULL;
  heap[heap_size++] = node;
 }
 
 // Floyd 建堆 O(n)
 for (int i = heap_size / 2 - 1; i >= 0; i--) {
  int idx = i;
  while (1) {
   int smallest = idx;
   int l = 2 * idx + 1, r = 2 * idx + 2;
   if (l < heap_size && heap[l]->freq < heap[smallest]->freq) smallest = l;
   if (r < heap_size && heap[r]->freq < heap[smallest]->freq) smallest = r;
   if (smallest == idx) break;
   HuffNode* t = heap[idx]; heap[idx] = heap[smallest]; heap[smallest] = t;
   idx = smallest;
  }
 }
 
 // 反复合并最小的两个
 while (heap_size > 1) {
  // extract min
  HuffNode* min1 = heap[0];
  heap[0] = heap[--heap_size];
  int idx = 0;
  while (1) {
   int smallest = idx;
   int l = 2 * idx + 1, r = 2 * idx + 2;
   if (l < heap_size && heap[l]->freq < heap[smallest]->freq) smallest = l;
   if (r < heap_size && heap[r]->freq < heap[smallest]->freq) smallest = r;
   if (smallest == idx) break;
   HuffNode* t = heap[idx]; heap[idx] = heap[smallest]; heap[smallest] = t;
   idx = smallest;
  }
 
  // extract min
  HuffNode* min2 = heap[0];
  heap[0] = heap[--heap_size];
  idx = 0;
  while (1) {
   int smallest = idx;
   int l = 2 * idx + 1, r = 2 * idx + 2;
   if (l < heap_size && heap[l]->freq < heap[smallest]->freq) smallest = l;
   if (r < heap_size && heap[r]->freq < heap[smallest]->freq) smallest = r;
   if (smallest == idx) break;
   HuffNode* t = heap[idx]; heap[idx] = heap[smallest]; heap[smallest] = t;
   idx = smallest;
  }
 
  // 合并
  HuffNode* parent = malloc(sizeof(HuffNode));
  parent->freq = min1->freq + min2->freq;
  parent->left = min1;
  parent->right = min2;
 
  // 插入堆并上浮
  heap[heap_size++] = parent;
  int pos = heap_size - 1;
  while (pos > 0) {
   int p = (pos - 1) / 2;
   if (heap[p]->freq <= heap[pos]->freq) break;
   HuffNode* t = heap[p]; heap[p] = heap[pos]; heap[pos] = t;
   pos = p;
  }
 }
 
 HuffNode* root = heap[0];
 free(heap);
 return root;
}

时间复杂度 次 extract + insert,每次 )。空间

树的其他重要变体

表达式树(Expression Tree):编译器将数学表达式解析为二叉树——操作数在叶子,运算符在内部节点。后序遍历即得到后缀表达式(RPN),可以直接求值。

graph TD
    MUL["*"] --> ADD["+"]
    MUL --> SUB["-"]
    ADD --> T3["3"]
    ADD --> T4["4"]
    SUB --> T5["5"]
    SUB --> T2["2"]
    style MUL fill:#4a90d9,color:#fff
    style ADD fill:#5ba3e6,color:#fff
    style SUB fill:#5ba3e6,color:#fff

后序遍历: 3 4 + 5 2 - * → 后缀表达式 → 求值结果 21

线索二叉树(Threaded Binary Tree):将空的 left/right 指针改为指向中序的前驱/后继的线索(thread)。遍历时无需递归或栈——从第一个节点出发,沿线索可以连续访问中序后继。内存开销为每个节点 2 bit(标记 left/right 是真指针还是线索)。线索二叉树将 空间的遍历栈需求压缩到常数空间。

线索二叉树详解

定义:对于二叉树中的空指针:

  • 若节点的 left 为空,令 left 指向其中序前驱(in-order predecessor)
  • 若节点的 right 为空,令 right 指向其中序后继(in-order successor)
  • 增加标志位 ltag/rtag:0 表示真子节点,1 表示线索

线索化过程:中序遍历一遍,在遍历过程中记录前一个访问的节点 prev。若当前节点 left 为空,则 left = prev(指向前驱);若 prevright 为空,则 prev->right = curr(指向后继)。

typedef struct ThreadNode {
 int data;
 struct ThreadNode *left, *right;
 int ltag, rtag; // 0=子节点, 1=线索
} ThreadNode;
 
static ThreadNode* prev = NULL;
 
void inorder_thread(ThreadNode* node) {
 if (!node) return;
 inorder_thread(node->left); // 左
 
 // 处理当前节点的线索
 if (!node->left) {
 node->ltag = 1;
 node->left = prev; // 指向前驱
 }
 if (prev && !prev->right) {
 prev->rtag = 1;
 prev->right = node; // 前驱的右指针指向当前(后继)
 }
 prev = node;
 
 inorder_thread(node->right); // 右
}

线索化手算轨迹

给定二叉树(中序序列为 D, B, A, E, C):

graph TD
    A["A"] --> B["B"]
    A --> C["C"]
    B --> D["D"]
    C --> E["E"]
当前节点leftright动作线索状态
1DNULLNULLD 的 left 指向 NULL(无前驱),D 的 right 指向 B(后继)D→B
2BDNULLB 的 right 指向 A(后继)D→B→A
3ABCA 的 left/right 都非空,无线索
4ENULLNULLE 的 left 指向 A(前驱),E 的 right 指向 C(后继)A→E→C
5CENULLC 的 left = E(已处理),C 的 right 指向 NULL(无后继)

最终线索化后,沿线索遍历:D → B → A → E → C(中序序列),无需栈。

自测:线索二叉树

二叉树中序序列为 G, D, H, B, E, A, C, F。① 画出线索二叉树(标注 ltag/rtag)。② 写出从节点 G 出发沿线索遍历的序列。

答案:

① 线索(ltag=1 表示线索,rtag=1 表示线索):

  • G: left=NULL(ltag=1,无前驱), right=D(rtag=1,后继)
  • D: left=G(ltag=1,前驱), right=H(rtag=1,后继)
  • H: left=D(ltag=1,前驱), right=B(rtag=1,后继)
  • B: left=D(ltag=1,前驱), right=E(rtag=1,后继)
  • E: left=B(ltag=1,前驱), right=A(rtag=1,后继)
  • A: left=B(ltag=1,前驱), right=C(rtag=1,后继)
  • C: left=A(ltag=1,前驱), right=F(rtag=1,后继)
  • F: left=C(ltag=1,前驱), right=NULL(rtag=1,无后继)

② 从 G 出发沿线索遍历:G → D → H → B → E → A → C → F

哈夫曼树(Huffman Tree):贪心构造的最优前缀编码二叉树。权值最小的两个节点不断合并为新节点,最终形成一棵最优的二叉树——高频字符靠近根,低频字符在深层。这是数据压缩的基础算法。

哈夫曼编码手算

给定字符频率表:a=5, b=9, c=12, d=13, e=16, f=45。

构建过程(每次合并两个最小频率):

合并结果频率剩余节点
1a(5) + b(9)(14)(14), c(12), d(13), e(16), f(45)
2c(12) + d(13)(25)(14), e(16), (25), f(45)
3(14) + e(16)(30)(25), (30), f(45)
4(25) + (30)(55)f(45), (55)
5f(45) + (55)(100)(100) ← 根

编码(左分支=0,右分支=1):

graph TD
    ROOT["(100)"] --> F["f=45"]
    ROOT --> N55["(55)"]
    N55 --> N25["(25)"]
    N55 --> N30["(30)"]
    N25 --> C["c=12"]
    N25 --> D["d=13"]
    N30 --> N14["(14)"]
    N30 --> E["e=16"]
    N14 --> A["a=5"]
    N14 --> B["b=9"]
字符编码频率x位数
f045x1 = 45
c10012x3 = 36
d10113x3 = 39
a11005x4 = 20
b11019x4 = 36
e11116x3 = 48

总加权路径长度 。若用等长编码(3 位/字符),总位数 。哈夫曼编码节省

自测:哈夫曼

频率表:A=27, B=8, C=15, D=15, E=30, F=5。构建哈夫曼树并写出 F 和 B 的编码。

答案:

步骤:F(5)+B(8)→13;13+C(15)→28;D(15)+A(27)→42;28+E(30)→58;42+58→100

graph TD
    ROOT["(100)"] --> N42["(42)"]
    ROOT --> N58["(58)"]
    N42 --> D["D=15"]
    N42 --> A["A=27"]
    N58 --> N28["(28)"]
    N58 --> E["E=30"]
    N28 --> N13["(13)"]
    N28 --> C["C=15"]
    N13 --> F["F=5"]
    N13 --> B["B=8"]

F 的编码:1000(根→右→左→左→左)
B 的编码:1001(根→右→左→左→右)


应用场景

  • 文件系统 (目录树):Linux 的 dentry cache 使用树结构组织路径。每个目录是一个节点,子目录/文件是子节点
  • 数据库索引:BST → AVL → 红黑树 → B+树。自平衡 BST 是 B+树的元实现基础。详见 红黑树B树
  • 编译器 语法树 (AST):编译器将源代码解析为抽象语法树——每个产生式规则对应一个节点,终结符在叶子
  • 域名系统 (DNS):域名本身就是一棵倒挂的树——根域 . → 顶级域 .com → 二级域 example → 主机名 www
  • 计算机网络 路由表:IP 路由的最长前缀匹配算法使用 Patricia Trie(压缩二叉 Trie)

练习

题号题目说明
94二叉树的中序遍历树遍历
98验证二叉搜索树BST 性质
108将有序数组转换为 BST构建平衡树
450删除二叉搜索树中的节点BST 删除三种情况
105从前序与中序构造二叉树前序+中序重建
222完全二叉树的节点个数树的性质

核心推演清单

练习题与上面的 LeetCode 互补——侧重手算推演,全部在正文中带完整答案:

自测位置内容
遍历序列 ×1 问遍历手算轨迹节四种遍历的序列输出
前序+中序重建 ×1 问重建手算节从序列画出二叉树
BST 删除 ×1 问BST 删除手算节三种情况的逐步操作
AVL 建树 ×1 问AVL 建树手算节逐步画树+标注旋转类型
哈夫曼编码 ×1 问哈夫曼手算节构建树+写出编码

动手实验

编号题目说明
E1四种遍历递归 vs 迭代实现中序遍历的递归版和迭代版(用显式栈),对随机生成的 1000 节点 BST 分别执行并计时。对比递归的隐式栈和迭代的显式栈的性能差异
E2BST 退化的渐进分析随机顺序插入 1..10000 到 BST,每插入 1000 个测量一次树高,画出”树高 vs 插入量”曲线。再以递增顺序(1,2,3…)插入同样值——观察退化链的高 = n。解释差距的数学来源
E3AVL 旋转计数随机插入 1..10000 到 AVL,统计 LL/RR/LR/RL 四种旋转各自发生了多少次。验证 LR 和 RL 的发生频率是否显著低于 LL 和 RR