链表是一维的,树把数据「竖」了起来。这一竖,查找从 O(n) 变成 O(log n) 成为可能,文件系统、数据库索引(第 9 章的 B+ 树就是多叉版)、编译器的语法树、DOM,全是树的形态。本章按「会走 → 会用 → 会平衡」三层推进:先把四种遍历练熟(递归思维的圣地),再学二叉搜索树(树为什么快),最后看两个平衡选手(AVL 管住高度,堆换个姿势二叉)。
二叉树节点比链表多一个指针:
typedef struct TNode {
int val;
struct TNode *left, *right;
} TNode;
全课最优雅的代码模板在这里:任何二叉树问题,先写递归三段式(空树返回什么、处理当前节点、递归左右子树),八成题目当场解掉。
| 遍历 | 顺序 | 典型用途 |
|---|---|---|
| 先序 | 根 → 左 → 右 | 复制树、序列化 |
| 中序 | 左 → 根 → 右 | BST 中序得有序序列 |
| 后序 | 左 → 右 → 根 | 释放树、计算依赖 |
| 层序 | 一层一层 | 求高度、BFS |
前三种的递归写法是三行互换(打印语句的位置决定是哪种):
void inorder(TNode *r) {
if (!r) return;
inorder(r->left); // 先左
printf("%d ", r->val); // 中(放最前=先序,最后=后序)
inorder(r->right); // 后右
}
层序遍历不用递归,用的是第 3 章的队列:出队一个,访问,把左右孩子入队。队列在这里的角色是「按层排队」,和 DFS 用栈「一条路走到黑」形成分野,这个「BFS 用队列、DFS 用栈」的对偶是图论章的前置知识。遍历的非递归版本(显式栈模拟)是面试常客,写一遍中序非递归,你对「递归 = 隐式栈」的理解会落到实处(计组第 10 章的栈帧,就是那口隐式栈)。
二叉搜索树给二叉树加了纪律:左子树全部小于根,根小于右子树全部。这条纪律让查找变成「每次丢一半」的猜数字:比根小往左,比根大往右,期望 O(log n):
TNode *insert(TNode *r, int v) {
if (!r) return new_node(v);
if (v < r->val) r->left = insert(r->left, v);
else if (v > r->val) r->right = insert(r->right, v);
return r; // 相等:按业务决定拒绝或计数
}
删除是 BST 的精华,三种情况:叶子直接摘;独子让儿子顶替;两个孩子,取中序后继(右子树最左节点,即「比它大的里面最小的」)的值顶上,再去删那个后继(它最多一个右孩子,落回前两种)。这个「找继承人」的思路保证了删除后 BST 的纪律不破。
BST 的软肋在同一行代码里:如果数据恰好递增插入,每次都往右走,树退化成链表,高度 n,一切操作回到 O(n)。有序数据插入 BST 是真实会发生的(按时间戳导入日志),所以需要自动平衡。
AVL 树在每个节点记平衡因子(左高减右高),插入删除后若某处因子绝对值超过 1,就地旋转修正,硬保证高度 O(log n)。四种失衡用「哪条腿长」记:LL(左子树的左边)和 RR 单旋一次;LR、RL 两次。旋转的图像感比公式重要:
RR 失衡(右腿长),左旋:
8 12
\ / \
12 → 8 15
/ \ \
10 15 10
动手推一次旋转,胜过看十张静态图(推荐去 VisuAlgo 网站拖一遍)。AVL 的性格是「严格」:任何节点失衡立刻修,查找路径最短;代价是插入删除时的旋转次数多。它的近亲红黑树(放宽到「最长路径不超过最短两倍」)旋转少,成了 STL map、Linux CFS(操作系统课第 3 章)、epoll 内部的选择,两种平衡性格对应两种负载,面试问到「为什么内核用红黑树不用 AVL」,答「更新频繁,红黑树调整便宜」就到位了。
堆放弃「全序」,只要一条:父 ≥ 子(大顶堆),而且必须是完全二叉树(每层从左到右填满)。完全这个约束送出一个大礼:可以用数组存,不用指针!父下标 i,孩子 2i+1 和 2i+2:
void sift_up(int *h, int i) { // 新元素在末尾,向上浮
while (i > 0 && h[(i - 1) / 2] < h[i]) {
int t = h[i]; h[i] = h[(i - 1) / 2]; h[(i - 1) / 2] = t;
i = (i - 1) / 2;
}
}
插入 O(log n)(上浮一条路径),取最值 O(1)(就在 h[0]),删除 O(log n)(末尾元素顶上来再下沉)。堆不承诺「中序有序」,它只回答一个问题:当前最大(小)的是谁。这正是优先队列的定义,操作系统的调度队列(第 3 章)、Dijkstra 的「下一个最近的点」(第 6 章)、定时器管理,全是它的用户。堆排序(第 8 章)就是「建堆 + 反复取顶」,原地 O(n log n),那时这章的代码原样复用。
一张决策表收束:要有序遍历和前驱后继,选 BST 系(AVL/红黑树);只要反复取最值,堆更省(数组存储、常数小);要范围查询和磁盘友好,那是第 9 章 B+ 树的故事。