红黑树(Red-Black Tree)是一种自平衡的二叉搜索树,它通过对每个节点进行着色(红色或黑色)并遵循特定规则,确保树的高度始终保持在对数级别,从而保证插入、删除和查找操作的时间复杂度为 O (log n)。本文将详细介绍红黑树的 Java 实现及其核心原理。
红黑树的基本性质
红黑树除了具备二叉搜索树的基本性质外,还需满足以下五个关键性质:
- 每个节点要么是红色,要么是黑色
- 根节点是黑色
- 所有叶子节点(NIL 节点,空节点)是黑色
- 如果一个节点是红色的,则它的两个子节点都是黑色的
- 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
这些性质确保了红黑树的关键特性:从根到叶子的最长路径不超过最短路径的两倍。这使得树大致上是平衡的。
红黑树的 Java 实现
下面是一个完整的红黑树 Java 实现,包含插入、删除、查找等核心操作:
public class RedBlackTree<T extends Comparable<T>> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private Node root;
private class Node {
T key;
Node left, right;
boolean color;
int size;
public Node(T key, boolean color, int size) {
this.key = key;
this.color = color;
this.size = size;
}
}
// 判断节点是否为红色
private boolean isRed(Node x) {
if (x == null) return false;
return x.color == RED;
}
// 返回树的大小
public int size() {
return size(root);
}
private int size(Node x) {
if (x == null) return 0;
return x.size;
}
// 左旋操作
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.size = h.size;
h.size = size(h.left) + size(h.right) + 1;
return x;
}
// 右旋操作
private Node rotateRight(Node h) {
Node x = h.left;
h.left = x.right;
x.right = h;
x.color = h.color;
h.color = RED;
x.size = h.size;
h.size = size(h.left) + size(h.right) + 1;
return x;
}
// 颜色转换
private void flipColors(Node h) {
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
// 插入操作
public void insert(T key) {
root = insert(root, key);
root.color = BLACK;
}
private Node insert(Node h, T key) {
if (h == null) return new Node(key, RED, 1);
int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = insert(h.left, key);
else if (cmp > 0) h.right = insert(h.right, key);
else h.key = key;
// 修复红黑树
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.size = size(h.left) + size(h.right) + 1;
return h;
}
// 查找操作
public boolean contains(T key) {
return get(key) != null;
}
public T get(T key) {
return get(root, key);
}
private T get(Node x, T key) {
while (x != null) {
int cmp = key.compareTo(x.key);
if (cmp < 0) x = x.left;
else if (cmp > 0) x = x.right;
else return x.key;
}
return null;
}
// 删除操作
public void delete(T key) {
if (!contains(key)) return;
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = delete(root, key);
if (!isEmpty()) root.color = BLACK;
}
private Node delete(Node h, T key) {
if (key.compareTo(h.key) < 0) {
if (!isRed(h.left) && !isRed(h.left.left))
h = moveRedLeft(h);
h.left = delete(h.left, key);
} else {
if (isRed(h.left))
h = rotateRight(h);
if (key.compareTo(h.key) == 0 && (h.right == null))
return null;
if (!isRed(h.right) && !isRed(h.right.left))
h = moveRedRight(h);
if (key.compareTo(h.key) == 0) {
Node x = min(h.right);
h.key = x.key;
h.right = deleteMin(h.right);
} else h.right = delete(h.right, key);
}
return balance(h);
}
// 辅助方法:移动红色到左侧
private Node moveRedLeft(Node h) {
flipColors(h);
if (isRed(h.right.left)) {
h.right = rotateRight(h.right);
h = rotateLeft(h);
flipColors(h);
}
return h;
}
// 辅助方法:修复红黑树
private Node balance(Node h) {
if (isRed(h.right)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.size = size(h.left) + size(h.right) + 1;
return h;
}
// 测试代码
public static void main(String[] args) {
RedBlackTree<Integer> tree = new RedBlackTree<>();
tree.insert(5);
tree.insert(3);
tree.insert(7);
tree.insert(2);
tree.insert(4);
tree.insert(6);
tree.insert(8);
System.out.print("中序遍历结果: ");
tree.inorderTraversal();
System.out.println("是否包含键 4: " + tree.contains(4));
System.out.println("是否包含键 9: " + tree.contains(9));
tree.delete(3);
System.out.print("删除键 3 后的中序遍历结果: ");
tree.inorderTraversal();
}
}
核心操作解析
旋转操作
旋转操作是红黑树保持平衡的关键,主要有左旋和右旋两种:
// 左旋操作
private Node rotateLeft(Node h) {
Node x = h.right;
h.right = x.left;
x.left = h;
x.color = h.color;
h.color = RED;
x.size = h.size;
h.size = size(h.left) + size(h.right) + 1;
return x;
}
左旋操作将节点 h 的右子节点 x 提升为新的父节点,并将 h 变为 x 的左子节点。同时调整节点的颜色和子树大小。右旋操作是对称的。
插入操作
插入操作首先按照二叉搜索树的方式插入新节点,并将其标记为红色。然后通过旋转和颜色转换来修复可能破坏的红黑树性质:
// 插入操作
public void insert(T key) {
root = insert(root, key);
root.color = BLACK;
}
private Node insert(Node h, T key) {
if (h == null) return new Node(key, RED, 1);
int cmp = key.compareTo(h.key);
if (cmp < 0) h.left = insert(h.left, key);
else if (cmp > 0) h.right = insert(h.right, key);
else h.key = key;
// 修复红黑树
if (isRed(h.right) && !isRed(h.left)) h = rotateLeft(h);
if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
if (isRed(h.left) && isRed(h.right)) flipColors(h);
h.size = size(h.left) + size(h.right) + 1;
return h;
}
插入修复主要处理三种情况:右子节点为红色而左子节点为黑色、左子节点和左子节点的左子节点均为红色、左右子节点均为红色。
删除操作
删除操作比插入更复杂,因为需要同时维护二叉搜索树和红黑树的性质:
// 删除操作
public void delete(T key) {
if (!contains(key)) return;
if (!isRed(root.left) && !isRed(root.right))
root.color = RED;
root = delete(root, key);
if (!isEmpty()) root.color = BLACK;
}
删除操作首先检查键是否存在,然后通过递归找到要删除的节点,并进行相应的调整。如果删除的节点是红色的,不会破坏红黑树的性质;如果是黑色的,则需要通过一系列调整来恢复性质。
红黑树通过牺牲一定的平衡性(相对于 AVL 树)来换取更高效的插入和删除操作,因此在实际应用中更为常见。