红黑树深度解析
一、什么是红黑树
红黑树是一种自平衡二叉搜索树,每个节点额外存储一位颜色(红或黑),通过颜色约束来保持树的近似平衡,保证最坏情况下操作的时间复杂度为 O(log n)。
为什么需要红黑树? 二叉搜索树在极端情况下退化为链表(O(n)),AVL 树平衡条件太严格导致频繁旋转。红黑树在两者之间取了折中。
二、红黑树的五条性质
| 编号 | 性质 | 说明 |
|---|---|---|
| 1 | 每个节点是红色或黑色 | - |
| 2 | 根节点是黑色 | - |
| 3 | NIL 叶子节点(空节点)是黑色 | - |
| 4 | 红色节点的两个子节点都是黑色 | 不能有连续红节点 |
| 5 | 从任一节点到其所有叶子节点的路径都包含相同数目的黑色节点 | 黑高相同 |
推论: 最长路径不超过最短路径的 2 倍。最短路径全黑,最长路径红黑交替。
B(13)
/ \
R(8) R(17)
/ \ / \
B(1) B(11) B(15) B(25)
/ \ / \ / \ / \
NIL NIL ... ... ...
三、核心操作
3.1 左旋与右旋
旋转是红黑树维持平衡的基本操作,不改变 BST 性质。
左旋(以 x 为轴): 右旋(以 y 为轴):
x y
/ \ / \
a y ←→ x c
/ \ / \
b c a b
java
// 左旋伪代码
void leftRotate(Node x) {
Node y = x.right;
x.right = y.left; // y 的左子树挂到 x 右边
if (y.left != NIL) y.left.parent = x;
y.parent = x.parent; // y 接替 x 的位置
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 变成 y 的左子
x.parent = y;
}
3.2 插入
规则:新插入的节点默认为红色(避免违反性质 5)。
插入后通过变色 + 旋转修复平衡:
| 情况 | 父节点 | 叔叔节点 | 操作 |
|---|---|---|---|
| 1 | - | - | 新节点是根 → 变黑 |
| 2 | 黑色 | - | 无需修复(不违反任何性质) |
| 3 | 红色 | 红色 | 父、叔变黑,祖父变红,递归修复祖父 |
| 4 | 红色(左) | 黑色/NIL | 父变黑,祖父变红,以祖父为轴右旋 |
| 5 | 红色(右) | 黑色/NIL | 先以父为轴左旋,转为情况 4 |
插入示例(依次插入 10, 20, 30):
1. 插入 10: [B(10)] → 根变黑
2. 插入 20: [B(10) → R(20)] → 父黑,无需修复
3. 插入 30: [B(10) → R(20) → R(30)] → 父红叔黑
修复: 左旋 10,变色
结果:
B(20)
/ \
R(10) R(30)
3.3 删除
删除比插入复杂,分两步:
- BST 删除:找到后继节点替换,实际被删除的最多只有一个子节点
- 修复平衡:如果删除的是黑色节点,违反性质 5,需要修复
修复核心思想:将兄弟节点的"多余黑色"转移上来。
| 情况 | 兄弟颜色 | 兄弟子节点 | 操作 |
|---|---|---|---|
| 1 | 红 | - | 兄变黑,父变红,旋转,转换其他情况 |
| 2 | 黑 | 两子均黑 | 兄变红,向上递归 |
| 3 | 黑 | 远子黑近子红 | 旋转使远子变红,转情况 4 |
| 4 | 黑 | 远子红 | 调整颜色 + 旋转,完成修复 |
四、红黑树 vs AVL 树
| 特性 | 红黑树 | AVL 树 |
|---|---|---|
| 平衡条件 | 最长路径 ≤ 2 倍最短路径 | 左右子树高度差 ≤ 1 |
| 插入旋转次数 | 最多 2 次 | 最多 O(log n) 次 |
| 删除旋转次数 | 最多 3 次 | 最多 O(log n) 次 |
| 查找性能 | 略慢(不那么平衡) | 更快(更严格平衡) |
| 适用场景 | 增删频繁(HashMap、TreeMap) | 查找密集(数据库内存索引) |
HashMap 为什么选红黑树? HashMap 中链表转树后,增删操作频繁(put/remove),红黑树增删性能更优。
五、Java 中的红黑树应用
5.1 TreeMap
java
TreeMap<String, Integer> map = new TreeMap<>();
map.put("banana", 2); // 红黑树插入,O(log n)
map.put("apple", 1);
map.put("cherry", 3);
// 利用有序性
map.firstKey(); // "apple"
map.lastKey(); // "cherry"
map.subMap("apple", "cherry"); // {"apple"=1, "banana"=2}
5.2 HashMap 的TreeNode
java
// HashMap 中链表转红黑树的阈值
static final int TREEIFY_THRESHOLD = 8; // 链表 → 红黑树
static final int UNTREEIFY_THRESHOLD = 6; // 红黑树 → 链表
static final int MIN_TREEIFY_CAPACITY = 64; // 数组最小长度(不够则扩容不树化)
5.3 TreeSet
java
// TreeSet 底层就是 TreeMap
TreeSet<Integer> set = new TreeSet<>();
set.add(3); set.add(1); set.add(2);
set.first(); // 1
set.last(); // 3
set.subSet(1, 3); // [1, 2]
六、面试高频题
Q1: 红黑树为什么比 AVL 树更适合 HashMap?
难度:⭐⭐⭐⭐
答案:HashMap 中增删频繁(put/remove 是常态),红黑树增删最多 2~3 次旋转,AVL 树可能需要 O(log n) 次旋转。虽然红黑树查找略慢,但 HashMap 场景下综合性能更优。
Q2: 红黑树插入新节点为什么是红色?
难度:⭐⭐⭐
答案:红色节点不影响黑高(性质 5),最多只违反性质 4(连续红节点),修复成本低。如果插入黑色节点,必然违反性质 5(路径黑高不等),修复更复杂。
Q3: HashMap 链表转红黑树的阈值为什么是 8?
难度:⭐⭐⭐⭐
答案:泊松分布计算,负载因子 0.75 时,链表长度达到 8 的概率仅为 0.00000006(亿分之六)。正常情况下不会触发树化,只有 hash 碰撞极端严重时才需要,是防御性设计。
Q4: 红黑树的查找、插入、删除时间复杂度?
难度:⭐⭐
答案:均为 O(log n)。红黑树保证最长路径不超过最短路径 2 倍,n 个节点的树高 h ≤ 2log₂(n+1),因此所有操作都是对数级别。