5.0 KiB
5.0 KiB
好的!先从“平衡树”的共同目标说起,再分别讲 AVL、红黑树、B-Tree,最后给一张对照表和选型建议。
什么是“平衡树”
“平衡树”都是在二叉搜索树(BST)或其多叉推广的框架上,通过限制高度来保证查找/插入/删除的时间复杂度稳定在 O(log n)
。不同平衡树的“平衡标准”不同,越严格,高度越低(查找更快),但维护成本(旋转/分裂/合并)会更高。
AVL 树(Adelson-Velsky and Landis)
核心不变量
- 对任一节点,
|height(left) - height(right)| ≤ 1
(平衡因子 ∈ {-1,0,+1})。 - 通常在节点里存高度或平衡因子,插删后自底向上更新。
失衡与旋转
- LL 型 → 右旋
- RR 型 → 左旋
- LR 型 → 先左旋子节点再右旋
- RL 型 → 先右旋子节点再左旋
插入至多 1 次“单/双旋”(最多 2 次旋转);删除可能沿路径多处修复(最坏
O(log n)
次)。
复杂度与性质
- 查找/插入/删除:
O(log n)
- 高度上界:约
≈ 1.44 * log2(n)
(非常低) - 优点:查找性能最稳定、树更矮
- 缺点:更新更“敏感”,旋转相对多;实现需维护高度
适用
- 查多改少或对最坏时延敏感的内存场景(如需要更低树高的索引、路由表等)
红黑树(Red-Black Tree)
核心不变量(颜色与黑高)
- 根为黑;2) 叶子(NIL)为黑;
- 红节点不能有红孩子(红红不相邻);
- 任一节点到其后代 NIL 的所有路径具有相同黑高。
插删修复
- 通过着色 + 旋转修复;
- 插入:至多 2 次旋转;删除:至多 3 次旋转(但可能多次“上溯”重着色)。
复杂度与性质
- 查找/插入/删除:
O(log n)
- 高度上界:
≤ 2 * log2(n+1)
(比 AVL 略高) - 优点:更新旋转次数少、实现成熟、工业广泛使用
- 缺点:查找平均高度略高于 AVL
适用
- 查改均衡的通用有序容器场景。C++ 的
std::map
/std::set
通常用红黑树实现;大多数语言标准库的有序映射/集合也选择红黑树。
B-Tree(多叉平衡搜索树)
(数据库/文件系统的主角,常用的是 B+ 树变体)
结构
- 每个节点可有最多
m
个孩子(多叉),节点里存有序键数组与孩子指针; - 所有叶子在同一层;
- 除根外:每个节点的键数在
[ceil(m/2)-1, m-1]
之间(保证“半满”)。
为何快(外存友好)
- 把一个节点的键打包成一页(如 4KB),一次 I/O 读入很多键;
- 分支因子大(几百级),高度极低,因此磁盘/SSD 的随机 I/O 次数很少。
- 复杂度常写作
O(log_m n)
(以分支因子为底),I/O 次数非常小。
插入/删除
- 插入:若目标节点满则分裂(把中间键上推给父节点),可能层层上推到根,根也满则新建根,高度+1;
- 删除:若节点“欠满”,先向兄弟借键,不行再合并,可能向上连锁。
B+ 与 B*
- B+ 树:所有数据都在叶子,内节点只存键;叶子链表天然支持范围扫描与顺序遍历(数据库最常见)。
- B*:更激进的“借/并”策略,节点利用率更高(≥ 2/3 满),进一步减少高度与 I/O。
适用
- 磁盘/SSD 上的大规模索引:数据库(如 InnoDB 使用 B+ 树)、文件系统、KV 存储等。
- 内存里也可用,但优势主要在外存局部性与范围扫描。
对照速览
维度 | AVL | 红黑树 | B-Tree / B+ |
---|---|---|---|
结构 | 二叉 | 二叉(带颜色) | 多叉(分支因子大) |
平衡标准 | 每节点左右高差 ≤ 1 | 黑高相等、红红不相邻 | 节点半满,叶子同层 |
高度 | 最矮(≈ 1.44 log₂ n) | 略高(≤ 2 log₂ n) | 极矮(logₘ n) |
查找 | 最稳定 | 稍逊于 AVL | 节点内二分后 I/O 极少 |
插入/删除 | 旋转较多 | 旋转较少 + 着色 | 分裂/合并,页级操作 |
范围扫描 | 好 | 好 | 最好(B+ 叶子链) |
典型场景 | 内存、查多改少 | 通用有序容器 | 外存索引/数据库 |
什么时候选谁?
- 内存、查多改少、追求极低高度 → 选 AVL。
- 内存、查改都多、工程通用与易用性 → 选 红黑树(标准库同款)。
- 磁盘/SSD、大数据、范围查询 → 选 B+ 树 / B-Tree(页友好,极少 I/O)。
如果你愿意,我可以再给你:
- AVL 和红黑树的旋转小抄(带 4 种失衡示意);
- B+ 树节点尺寸推算(给定页大小、键宽/指针宽,估算分支因子与高度);
- 一段C++ 伪代码/最小实现骨架帮助你在题目里自写平衡树。