加载中…
应用场景:语言标准库(TreeMap) · 工程首选平衡树
交互式动画演示,可调整参数并单步执行。
全屏打开普通 BST 在极端情况下会退化成链表(O(n) 操作)。
平衡 BST 通过旋转操作保证树高始终为 O(log n),从而保证所有操作的对数复杂度。
主流平衡 BST 对比:
| 树 | 严格平衡? | 插入删除效率 | 工程应用 |
|---|---|---|---|
| AVL | ✅ 严格 | 旋转多,删除成本高 | 数据库索引(读密集) |
| 红黑树 | ❌ 近似 | 旋转少 | Java TreeMap, C++ STL map |
| 2-3 树 / 2-3-4 树 | ✅ 严格 | 复杂 | B 树前身 |
| B 树 / B+ 树 | ✅(页级) | 适合外存 | 数据库、文件系统 |
| 跳表 | ❌ 概率 | 简单 | Redis ZSet |
红黑树是每个节点带颜色的二叉搜索树,满足 5 条性质:
由这些性质推出:最长路径 ≤ 2 × 最短路径 → 树高 O(log n)。
假设黑高 = h。
- 最短路径:全黑 → h 个节点。
- 最长路径:红黑交替 → 2h 个节点。
比值 ≤ 2 → 树是"近似平衡"。
| 情况 | uncle 颜色 | 操作 |
|---|---|---|
| 1 | 红 | 父和 uncle 变黑,祖父变红,向上递归 |
| 2 | 黑 / NIL | 父是祖父左子、自己是父右子 → 左旋父(变情况 3) |
| 3 | 黑 / NIL | 父变黑,祖父变红,以祖父为支点右旋 |
删除比插入复杂得多,分6 种情况。核心思路:
6 种情况分类(设兄弟为 s):
| 情况 | 兄弟状态 | 操作 |
|---|---|---|
| 1 | s 红 | s 变黑,父变红,左旋父(变情况 2/3/4) |
| 2 | s 黑,s 两子黑 | s 变红,向上递归 |
| 3 | s 黑,s 左红右黑 | s 变红,s 左子变黑,右旋 s(变情况 4) |
| 4 | s 黑,s 右红 | s 继承父颜色,父变黑,s 右子变黑,左旋父 |
红黑树的删除正确性证明是数据结构课最难的环节之一。
enum Color { RED, BLACK }
class Node {
int key;
Color color = Color.RED; // 新节点默认红色
Node left, right, parent;
Node(int key) { this.key = key; }
}
public class RBTree {
private Node root;
private final Node NIL = new Node(0); // 哨兵
{ NIL.color = Color.BLACK; }
private void leftRotate(Node x) {
Node y = x.right;
x.right = y.left;
if (y.left != NIL) y.left.parent = x;
y.parent = x.parent;
if (x.parent == NIL) root = y;
else if (x == x.parent.left) x.parent.left = y;
else x.parent.right = y;
y.left = x;
x.parent = y;
}
private void rightRotate(Node y) {
Node x = y.left;
y.left = x.right;
if (x.right != NIL) x.right.parent = y;
x.parent = y.parent;
if (y.parent == NIL) root = x;
else if (y == y.parent.right) y.parent.right = x;
else y.parent.left = x;
x.right = y;
y.parent = x;
}
private void fixInsert(Node z) {
while (z.parent.color == Color.RED) {
Node gp = z.parent.parent;
if (z.parent == gp.left) {
Node uncle = gp.right;
if (uncle.color == Color.RED) { // 情况 1
z.parent.color = Color.BLACK;
uncle.color = Color.BLACK;
gp.color = Color.RED;
z = gp;
} else {
if (z == z.parent.right) { // 情况 2 → 转情况 3
z = z.parent;
leftRotate(z);
}
z.parent.color = Color.BLACK; // 情况 3
gp.color = Color.RED;
rightRotate(gp);
}
} else { /* 对称情形:父是祖父右子 */ }
}
root.color = Color.BLACK;
}
private void fixDelete(Node x) {
while (x != root && x.color == Color.BLACK) {
if (x == x.parent.left) {
Node w = x.parent.right; // 兄弟
if (w.color == Color.RED) { // 情况 1
w.color = Color.BLACK;
x.parent.color = Color.RED;
leftRotate(x.parent);
w = x.parent.right;
}
if (w.left.color == Color.BLACK && w.right.color == Color.BLACK) { // 情况 2
w.color = Color.RED;
x = x.parent;
} else {
if (w.right.color == Color.BLACK) { // 情况 3 → 转 4
w.left.color = Color.BLACK;
w.color = Color.RED;
rightRotate(w);
w = x.parent.right;
}
w.color = x.parent.color; // 情况 4
x.parent.color = Color.BLACK;
w.right.color = Color.BLACK;
leftRotate(x.parent);
x = root;
}
} else { /* 对称情形 */ }
}
x.color = Color.BLACK;
}
}| 操作 | 时间 |
|---|---|
| 查找 | O(log n) |
| 插入 | O(log n) |
| 删除 | O(log n) |
| 旋转次数 | ≤ 2(插入)/ ≤ 3(删除) |
关键事实:红黑树的插入最多 2 次旋转,删除最多 3 次旋转。AVL 树的删除可能更多。
| 系统 | 使用红黑树的位置 |
|---|---|
| Java | TreeMap, TreeSet |
| C++ STL | std::map, std::set |
| Linux 内核 | CFS 调度器、虚拟内存管理 |
| Nginx | 定时器管理 |
| epoll | 事件回调管理 |
为什么选红黑树而不是 AVL?
红黑树的插入/删除旋转更少(≤ 3 次),而 AVL 树为了严格平衡需要更多旋转。对于写密集场景,红黑树性能更稳定。
| 场景 | 推荐 |
|---|---|
| 读多写少 | AVL(更严格的平衡) |
| 写多读少 | 红黑树(更少的旋转) |
| 通用 | 红黑树(工业界默认) |
旋转和删除时,需要判断
null还是叶子,统一用 NIL 哨兵可以让边界条件统一处理。
性质 4 + 性质 5:路径上红黑交替上限(红不连续),且黑节点数固定。最长全是红黑交替,最短全是黑。比值 = 2。
左红右黑不能连续,红黑交替算平衡。
插入修复三情况,叔叔红则染色,叔叔黑则旋转。
删除修复四情况,兄弟红则染色旋转,兄弟黑看侄子。