红黑树的性质
红黑树是一种自平衡的二叉搜索树,它通过在每个节点上增加一个表示颜色的属性来保证树的大致平衡。红黑树有五个基本性质,这些性质共同确保了树的高度始终保持在 O(log n),从而保证了基本操作的时间复杂度为 O(log n)。
红黑树的五个基本性质:
- 节点颜色 每个节点要么是红色,要么是黑色。
- 根节点 根节点总是黑色的。
- 叶节点(NIL节点) 每个叶节点(NIL节点,空节点)都是黑色的。 注意:在许多实现中,叶节点会使用一个特殊的黑色哨兵节点来表示。
- 红色节点的子节点 如果一个节点是红色的,则它的两个子节点都必须是黑色的。 (也就是说,不能有两个连续的红色节点)
- 黑色深度对于每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
- 前提需要是二叉搜索树
简单总结记忆口诀:左根右,根叶黑,不红红,黑路同
性质的作用:
- 性质 4 确保了从根到叶子的最长可能路径不会超过最短可能路径的两倍长。因为最短的路径全是黑色节点,最长的路径是红黑相间的。
- 性质 5 确保了树是大致平衡的。因为任意简单路径上黑色节点的数量相同,这就限制了树可能的不平衡程度。
- 这些性质共同确保了树的高度始终保持在 O(log n),其中 n 是树中节点的数量。
- 由于树的高度被限制在 O(log n),所以基本操作(如插入、删除和查找)的时间复杂度也就被限制在 O(log n)。
实际操作:
- 插入操作:新插入的节点初始为红色。插入后,可能需要通过重新着色和旋转来恢复红黑树的性质。
- 删除操作:删除节点后,可能需要进行一系列的重新着色和旋转操作来恢复红黑树的性质。
- 查找操作:与普通的二叉搜索树相同。
重新着色和旋转来恢复红黑树的性质:
- 插入新节点
首先,我们像在普通的二叉搜索树中那样插入新节点,并将其染成红色。为什么是红色?因为这样不会违反性质5(每条路径上的黑色节点数量相同)。
- 检查和修复
插入后,我们需要检查是否违反了红黑树的性质,主要关注是否出现了连续的红色节点。我们从新插入的节点开始,向上检查。
- 情况分析
我们主要关注新节点(我们称为N)、它的父节点(P)、叔叔节点(U,即父节点的兄弟)和祖父节点(G)。
情况 1:N是根节点
- 直接将N染成黑色。完成!
情况 2:P是黑色
- 不需要做任何事。红黑树的性质没有被违反。完成!
情况 3:P是红色(这意味着G一定存在,且为黑色) 这里我们需要考虑U的颜色:
a. U是红色:
- 将P和U都染成黑色
- 将G染成红色
- 将G作为新的N节点,继续向上检查
b. U是黑色(或不存在): 这里有四种子情况,取决于N和P相对于G的位置:
- 左左(LL):P是G的左子节点,N是P的左子节点
- 左右(LR):P是G的左子节点,N是P的右子节点
- 右右(RR):P是G的右子节点,N是P的右子节点
- 右左(RL):P是G的右子节点,N是P的左子节点
对于LL情况:
- 右旋G
- 交换P和G的颜色
对于RR情况:
- 左旋G
- 交换P和G的颜色
对于LR情况:
- 左旋P
- 将新的情况当作LL处理
对于RL情况:
- 右旋P
- 将新的情况当作RR处理
- 重复检查
每次调整后,我们都要继续向上检查,直到根节点,确保整棵树都符合红黑树的性质。
检查完成后,总是将根节点染成黑色,以确保性质2(根是黑色)成立。
这个过程看起来复杂,但实际上就是不断检查和调整,直到所有的红黑树性质都得到满足。我们只要时刻牢记关键的性质:
- 新节点总是红色
- 我们主要关心连续的红色节点
- 叔叔的颜色决定了我们是重新着色还是旋转
- 旋转用于平衡树,着色用于保持性质
- 最后总是确保根是黑色的
红黑树的代码基本实现
public class RedBlackTree<T extends Comparable<T>> {
// 定义红黑树节点的颜色
private static final boolean RED = true;
private static final boolean BLACK = false;
// 内部节点类
private class Node {
T data; // 节点存储的数据
Node left, right, parent; // 左子节点、右子节点和父节点
boolean color; // 节点的颜色
Node(T data) {
this.data = data;
this.color = RED; // 新节点默认为红色
}
}
private Node root; // 红黑树的根节点
// 插入操作的公共方法
public void insert(T data) {
Node node = new Node(data); // 创建新节点
root = insert(root, node); // 插入节点
fixViolation(node); // 修复可能违反的红黑树性质
}
// 递归插入节点的私有方法
private Node insert(Node root, Node node) {
if (root == null) return node; // 如果树为空,新节点成为根节点
// 根据比较结果决定插入到左子树还是右子树
if (node.data.compareTo(root.data) < 0) {
root.left = insert(root.left, node);
root.left.parent = root;
} else if (node.data.compareTo(root.data) > 0) {
root.right = insert(root.right, node);
root.right.parent = root;
}
return root;
}
// 修复红黑树性质的方法
private void fixViolation(Node node) {
Node parent = null;
Node grandParent = null;
// 当节点不是根节点,且节点是红色,其父节点也是红色时,需要修复
while (node != root && node.color != BLACK && node.parent.color == RED) {
parent = node.parent;
grandParent = parent.parent;
if (parent == grandParent.left) { // 父节点是祖父节点的左子节点
Node uncle = grandParent.right;
if (uncle != null && uncle.color == RED) {
// Case 1: 叔叔节点是红色
grandParent.color = RED;
parent.color = BLACK;
uncle.color = BLACK;
node = grandParent;
} else {
if (node == parent.right) {
// Case 2: 节点是父节点的右子节点
rotateLeft(parent);
node = parent;
parent = node.parent;
}
// Case 3: 节点是父节点的左子节点
rotateRight(grandParent);
boolean tempColor = parent.color;
parent.color = grandParent.color;
grandParent.color = tempColor;
node = parent;
}
} else { // 父节点是祖父节点的右子节点
Node uncle = grandParent.left;
if (uncle != null && uncle.color == RED) {
// Case 1: 叔叔节点是红色
grandParent.color = RED;
parent.color = BLACK;
uncle.color = BLACK;
node = grandParent;
} else {
if (node == parent.left) {
// Case 2: 节点是父节点的左子节点
rotateRight(parent);
node = parent;
parent = node.parent;
}
// Case 3: 节点是父节点的右子节点
rotateLeft(grandParent);
boolean tempColor = parent.color;
parent.color = grandParent.color;
grandParent.color = tempColor;
node = parent;
}
}
}
root.color = BLACK; // 确保根节点始终为黑色
}
// 左旋操作
private void rotateLeft(Node node) {
Node rightChild = node.right;
node.right = rightChild.left;
if (node.right != null)
node.right.parent = node;
rightChild.parent = node.parent;
if (node.parent == null)
root = rightChild;
else if (node == node.parent.left)
node.parent.left = rightChild;
else
node.parent.right = rightChild;
rightChild.left = node;
node.parent = rightChild;
}
// 右旋操作
private void rotateRight(Node node) {
Node leftChild = node.left;
node.left = leftChild.right;
if (node.left != null)
node.left.parent = node;
leftChild.parent = node.parent;
if (node.parent == null)
root = leftChild;
else if (node == node.parent.right)
node.parent.right = leftChild;
else
node.parent.left = leftChild;
leftChild.right = node;
node.parent = leftChild;
}
// 其他方法如删除、查找等可以在这里实现
}
HashSet和HashMap
HashSet和HashMap在Java中都使用了红黑树作为其底层数据结构的一部分,但它们的使用方式略有不同。
-
HashMap 中的红黑树:
HashMap 主要使用数组 + 链表 + 红黑树的结构:
- 基本结构是一个数组,数组的每个元素是一个桶(bucket)。
- 当发生哈希冲突时,相同哈希值的元素会被放到同一个桶中,形成一个链表。
- 当一个桶中的元素数量超过一定阈值(默认为8)且数组长度大于等于64时,这个链表会被转换为红黑树。
具体实现:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
// ... 其他方法
}
转换过程
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 如果数组为空或长度小于最小树化容量,则进行扩容而不是树化
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
// 计算桶的索引,并获取该桶的第一个节点
TreeNode<K,V> hd = null, tl = null; // 初始化树的头节点和尾节点
do {
// 将当前链表节点转换为TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p; // 如果是第一个节点,设置为头节点
else {
p.prev = tl; // 设置新节点的前驱
tl.next = p; // 将新节点链接到链表末尾
}
tl = p; // 更新尾节点为当前节点
} while ((e = e.next) != null); // 遍历链表中的所有节点
// 将转换后的TreeNode链表的头节点设置为该桶的第一个元素
if ((tab[index] = hd) != null)
hd.treeify(tab); // 调用treeify方法将链表结构转换为红黑树结构
}
}
-
HashSet 中的红黑树:
HashSet 实际上是基于 HashMap 实现的。它的所有操作都是通过内部的 HashMap 来完成的。
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// ... 其他方法
}
因此,HashSet 中的红黑树实现实际上就是 HashMap 的实现。
主要区别:
- HashMap 直接使用红黑树来存储键值对。
- HashSet 通过 HashMap 间接使用红黑树,只存储键(HashSet 的元素),值都是同一个虚拟对象 PRESENT。
使用红黑树的好处:
- 提高性能:当桶中元素较多时,红黑树的查询性能(O(log n))比链表(O(n))更好。
- 平衡:红黑树保证了各种操作的最坏情况时间复杂度为 O(log n)。