红黑树(Red Black Tree) 是一种自平衡二叉查找树,相对于普通的二叉树具有通过自旋和变色来保持树两端保持平衡的特点,从而获得较高的查找性能。
红黑树的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除。
-
二叉查找树
在正式介绍红黑树前,先简要介绍下二叉查找树(BST),二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
- 若左子树不空,则左子树上所有节点的值均小于它的根节
- 若右子树不空,则右子树上所有节点的值均大于它的根节点的值
- 左、右子树也分别为二叉排序树
- 没有键值相等的节点(这个看实际需求,非固定)
如下图就是一个典型的二叉查找树

那么这种树有什么好处呢,如同它的名字一般,它对查找非常便利,在上面数据中如果你想查找是否存在11,你只需要分别和10,15,13,11比较就可以得出,数据越多,它查找的优势就更体现出来。它查找所需的最大次数等同于二叉查找树的高度。
但是它也有缺陷所在,当构成它的数据导致树两端不平衡时,查找性能就大打折扣了

当插入的数据是有序的时候,生成的二叉树就类似与一个链表,这种情况下查找时,就需要遍历全部数据
-
总结
- 最好的情况是 O(logn):在数据符合完全二叉树类似情况下,其查找性能接近于二分查找,理想的树的高度为logN。
- 最差时候会是 O(n):极端情况下比如插入的数据是有序的,生成的二叉查找树就是一个链表,这样树的高度就为N。
-
红黑树
基于上面提到BST存在的问题,一种新的树–平衡二叉树(Balanced BST)被提了出来。平衡二叉树在插入和删除的时候,会通过旋转操作将树的高度一直保持在logN。具有代表性的平衡二叉树有两种,分别为AVL树和红黑树,AVL树因为性能更差的缘故,在实际运用的情况下远不如红黑树。
红黑树(Red-Black Tree,以下简称RBTree)的实际应用非常广泛,常见的函数库,如C++ STL中,很多部分(包括set, multiset, map, multimap)应用了红黑树的变体,以及Java中的TreeMap,TreeSet, Java8中的HashMap的实现也将链表替换成了红黑树。
RBTree为了保持树严格的平衡性质,在原来BST的基础上添加了一下五点性质:
- 根节点是黑色。
- 树的任一节点是红色或黑色。
- 每个红色节点的两个子节点都是黑色。
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
- 空节点默认是黑色的
class TreeMapEntry{
K key;
V value;
TreeMapEntry<K,V> left;
TreeMapEntry<K,V> right;
TreeMapEntry<K,V> parent;
boolean color = BLACK;
}
RBTree节点的数据结构
RBTree在本质上还是一棵BST树,但是它在插入和删除数据的时候会通过变色和自旋来保持树的平衡,即保证树的高度在[logN,logN+1],将树的查找时间复杂度始终保持在logN,同时RBTree的插入和删除时间复杂度也都是logN,所以RBTree的查找接近于理想的BST。
-
红黑树的自旋
RBTree的自旋主要目的是为了让节点的颜色符合上面的性质,从而使树的高度达到平衡。RBTree的旋转分为左旋和右旋,左边子节点升到父节点位置为右旋,右边子节点升到父节点为左旋。

如上图中,当某个节点(100)左旋时,它自身往右降一级,左节点不变,右节点(150)断开然后连上右节点(150)的左节点(125),然后右节点(150)当家做主称为父节点,至此完成一个左旋

同理,当某个节点(100)右旋时,它自身往左降一级,右节点不变,左节点(50)断开然后连上左节点(50)的右节点(75),然后左节点(50)当家做主称为父节点,至此完成一个右旋
RBTree的自旋主要是因为插入或删除后节点的颜色不符合上述的五条性质,导致树整体不平衡,需要通过自旋对树进行降层保持树的平衡
Java的RBTree就是一个典型的红黑树例子,下面也就TreeMap的源码来对解析RBTree的插入和删除操作
每个新插入的节点都是红色的,如果插入的父节点是黑色的,那么操作结束。如果父节点是红色,那么则违反了规则3:每个红色节点的两个子节点都是黑色,则需要改变父类的颜色,如果父类颜色和祖父类冲突,那么就需要继续变色,甚至是自旋来使树节点颜色符合规则。
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
// 如果当前没有数据,就用此点当做根节点
if (t == null) {
compare(key, key);
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
TreeMapEntry<K,V> parent;
// 比较大小的方式,如果已经自定义过就用自己设定的,不然就用系统默认的比较方式
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
//找到插入的父节点,生成当街节点,然后根据大小放在左节点还是右节点
TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
// 数据插入完成后开始对树进行颜色平衡处理
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
fixAfterInsertion
//这里对颜色的处理其实只看一半就行了,你会发现else后面的代码和上面是一样的,只不过
//左右做一下镜像处理
private void fixAfterInsertion(TreeMapEntry<K,V> x) {
//新添加的节点设为红色
本文介绍了红黑树作为自平衡二叉查找树的特性,对比了二叉查找树的优缺点,并详细阐述了红黑树如何通过自旋和变色保持树的平衡,确保查找、插入和删除的时间复杂度为O(log n)。通过Java中的TreeMap为例,解释了红黑树的插入和删除操作,包括变色和旋转的规则,帮助读者深入理解红黑树的运作机制。
最低0.47元/天 解锁文章
707

被折叠的 条评论
为什么被折叠?



