在计算机科学中,平衡二叉搜索树是许多高效数据结构的基础。红黑树(Red-Black Tree)作为一种自平衡二叉搜索树,在保证操作时间复杂度为O(log n)的同时,通过巧妙的颜色约束实现了相对较低的维护成本。本文将深入剖析红黑树的原理、核心操作,并结合Java集合框架中的TreeMap源码,带你彻底掌握这一经典数据结构。
一、为什么需要红黑树?
二叉搜索树在极端情况下会退化为链表,导致操作复杂度降至O(n)。为了保持平衡,人们发明了多种平衡树,如AVL树、红黑树等。与AVL树严格的高度平衡不同,红黑树通过"近似平衡"来减少旋转次数,从而在插入、删除频繁的场景下表现更优。Java的TreeMap、TreeSet以及HashMap中的链表树化(当链表长度超过阈值时,会将链表转换为红黑树)都基于红黑树实现。
二、红黑树的定义与性质
红黑树是一棵满足以下五条性质的二叉搜索树:
-
每个节点是红色或黑色。
-
根节点是黑色。
-
每个叶子节点(NIL节点,即空节点)是黑色。
通常实现中,我们用
null代表叶子节点,并认为其颜色为黑色。 -
如果一个节点是红色,则它的两个子节点都是黑色。
这条性质保证了没有两个连续的红色节点。
-
对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
这条性质称为"黑色平衡",它确保了红黑树的最长路径不超过最短路径的两倍,从而保证了操作的时间复杂度为O(log n)。
这些性质共同保证了红黑树的高度最多为2log₂(n+1),因此查找、插入、删除的时间复杂度均为O(log n)。
三、红黑树节点定义
在Java中,红黑树节点通常包含键、值、左子节点、右子节点、父节点以及颜色标记。以TreeMap中的Entry类为例:
java
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK; // 默认为黑色
// 构造函数、getter、setter等省略
}
四、核心操作:旋转
红黑树的插入和删除操作会破坏其性质,需要通过重新着色和旋转来修复。旋转分为左旋和右旋,它们是局部调整树结构的原子操作,不会改变二叉搜索树的性质。
左旋
java
// 左旋操作:将节点p向右旋转,使p的右子节点成为新的父节点
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
右旋
java
// 右旋操作:将节点p向左旋转,使p的左子节点成为新的父节点
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null)
l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else
p.parent.left = l;
l.right = p;
p.parent = l;
}
}
五、插入操作与修复
插入新节点时,我们首先按照二叉搜索树的规则找到插入位置,然后将新节点染成红色(这样不会破坏性质5,但可能破坏性质4)。如果父节点是黑色,插入完成;如果父节点是红色,则出现连续红色,需要根据叔叔节点的颜色进行修复。
插入修复的三种情况(假设新节点为z,父节点为p,祖父节点为g,叔叔节点为u):
-
叔叔节点u是红色
将p和u染黑,g染红,然后将z指向g,继续向上修复。
-
叔叔节点u是黑色,且z是p的右子节点(LL双红)
对p进行左旋,转化为情况3,然后按情况3处理。
-
叔叔节点u是黑色,且z是p的左子节点(LR双红)
将p染黑,g染红,然后对g进行右旋。
插入修复代码(来自TreeMap)
java
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) { // 情况1:叔叔为红色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == rightOf(parentOf(x))) { // 情况2:当前节点是右孩子
x = parentOf(x);
rotateLeft(x);
}
// 情况3:当前节点是左孩子
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
// 对称处理
// ...
}
}
root.color = BLACK;
}
六、删除操作与修复
删除操作相对复杂。首先找到实际被删除的节点(如果待删除节点有两个子节点,则用后继节点替换,然后转为删除后继节点)。删除后,如果被删除节点是黑色,则会破坏性质5,需要进行修复。
删除修复的核心是处理"双黑"节点,通过旋转和着色恢复平衡。修复过程根据兄弟节点的颜色和子节点的情况分为多种情形。由于篇幅所限,这里只列出修复的主要思想,具体可参考TreeMap中的fixAfterDeletion方法。
七、红黑树 vs. AVL树
| 特性 | 红黑树 | AVL树 |
|---|---|---|
| 平衡条件 | 近似平衡(最长路径不超过最短路径的两倍) | 严格平衡(左右子树高度差≤1) |
| 查找效率 | O(log n),略逊于AVL | O(log n),更严格,查找更快 |
| 插入/删除效率 | 旋转次数少,平均性能更好 | 可能需要多次旋转,但总体仍是O(log n) |
| 适用场景 | 插入删除频繁,如TreeMap、HashMap内部 |
查找操作远多于插入删除的场景 |
八、Java中的红黑树应用
1. TreeMap和TreeSet
TreeMap使用红黑树存储键值对,所有操作(put、get、remove)都依赖于红黑树的算法。TreeSet则基于TreeMap实现,只使用键。
2. HashMap中的树化
当HashMap中某个桶的链表长度超过TREEIFY_THRESHOLD(默认8)时,会将链表转换为红黑树,以提高查找效率。当红黑树节点数量减少到UNTREEIFY_THRESHOLD(默认6)时,又会退化为链表。
九、示例:手写一个简单的红黑树
下面给出一个极简的红黑树实现,只包含插入操作,帮助理解核心流程。
java
public class RBTree<K extends Comparable<K>, V> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;
private class Node {
K key;
V value;
Node left, right, parent;
boolean color;
Node(K key, V value, Node parent) {
this.key = key;
this.value = value;
this.parent = parent;
this.color = RED; // 新节点为红色
}
}
public void put(K key, V value) {
if (root == null) {
root = new Node(key, value, null);
root.color = BLACK;
return;
}
Node parent = null;
Node cur = root;
while (cur != null) {
parent = cur;
int cmp = key.compareTo(cur.key);
if (cmp < 0) cur = cur.left;
else if (cmp > 0) cur = cur.right;
else {
cur.value = value; // 更新值
return;
}
}
Node newNode = new Node(key, value, parent);
if (key.compareTo(parent.key) < 0) parent.left = newNode;
else parent.right = newNode;
fixAfterInsertion(newNode);
}
private void fixAfterInsertion(Node x) {
x.color = RED;
while (x != null && x != root && x.parent.color == RED) {
if (x.parent == x.parent.parent.left) {
Node y = x.parent.parent.right; // 叔叔节点
if (y != null && y.color == RED) {
// 情况1
x.parent.color = BLACK;
y.color = BLACK;
x.parent.parent.color = RED;
x = x.parent.parent;
} else {
if (x == x.parent.right) {
// 情况2
x = x.parent;
rotateLeft(x);
}
// 情况3
x.parent.color = BLACK;
x.parent.parent.color = RED;
rotateRight(x.parent.parent);
}
} else {
// 对称处理
Node y = x.parent.parent.left;
if (y != null && y.color == RED) {
x.parent.color = BLACK;
y.color = BLACK;
x.parent.parent.color = RED;
x = x.parent.parent;
} else {
if (x == x.parent.left) {
x = x.parent;
rotateRight(x);
}
x.parent.color = BLACK;
x.parent.parent.color = RED;
rotateLeft(x.parent.parent);
}
}
}
root.color = BLACK;
}
// 左旋与右旋实现(与TreeMap类似,此处省略具体代码)
private void rotateLeft(Node p) { /* 实现略 */ }
private void rotateRight(Node p) { /* 实现略 */ }
}
十、总结
红黑树通过引入颜色属性和五条约束,实现了近似平衡的二叉搜索树。其插入、删除操作虽然需要处理多种情况,但通过旋转和着色可以在常数次操作内恢复平衡。在Java中,TreeMap、TreeSet和HashMap(在冲突严重时)都依赖于红黑树,是学习数据结构与算法的经典范例。理解红黑树不仅能帮助我们更好地使用这些集合类,也为设计高性能的数据结构提供了坚实的理论基础。