红黑树
1.基本知识:
1.1什么是红黑树?
红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一陪.
和AVL树相比,AVL树是一棵严格的平衡树(它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1),它的所有子数都满足平衡二叉树的定义.因此,AVL树的查找效率很高,但是插入,删除旋转次数比红黑树多.
1.2红黑树的性质:
a.每个结点都是红色或者黑色;
b.根结点是黑色.
c.每个叶结点都是黑色(注意这里的叶结点都是指空结点);
d.如果一个结点是红色的,则它的两个儿子是黑色的;
e.对于每一个结点,从该结点到其叶子结点构成的所有路径上的黑结点个数相同.
2.TreeSet and TreeMap
2.1总体介绍
红黑树应用之一就是Java集合类中TreeSet
和TreeMap
的实现.以TreeMap
的源码为例来讲解红黑树.
我们上边学习了红黑树的性质,但是当我们进行插入或者删除操作时,性质d,c可能会被破坏,因此我们需要进行调整使它成为一颗合格的红黑树.
调整的方法一般有两类:颜色调整和结构调整(左旋,右旋).
2.2预备知识
2.2.1左旋
左旋的过程是将x
的右子树绕x
逆时针旋转,使得x
的右子树成为x
的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
(图片暂时没有)
TreeMap
源代码:
1 | private void rotateLeft(TreeMap.Entry<K, V> p) { |
2.2.2右旋
右旋的过程是将x
的左子树绕x
顺时针旋转,使得x
的左子树成为x
的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。
TreeMap
源代码:
1 | private void rotateRight(TreeMap.Entry<K, V> p) { |
2.2.3寻找结点后继
对于一棵二叉查找树,给定节点t,其后继(树中比大于t的最小的那个元素)可以通过如下方式找到:
- t的右子树不空,则t的后继是其右子树中最小的那个元素(右子树中最左边的)。
- t的右孩子为空,则t的后继是其第一个向左走的祖先。
后继节点在红黑树的删除操作中将会用到。
TreeMap
源代码:
1 | static <K, V> TreeMap.Entry<K, V> successor(TreeMap.Entry<K, V> t) { |
2.3主要方法分析
2.3.1 get()
get(Object key)
根据key
找到指定键值的value
,或者null
.该方法中核心是通过getEntry(Object key)
来的到entry
,然后返回entry.value
.
算法思想是根据key
的自然顺序(或比较器顺序)对二叉查找树进行查找,直到找到满足k.compareTo(p.key) == 0
的entry
.
get()
源码:
1 | public V get(Object key) { |
getEntry()
源码:
1 | final TreeMap.Entry<K, V> getEntry(Object key) { |
2.3.2 put()
put(K key, V value)
方法是将指定的key
, value
对添加到map
里。该方法首先会对map
做一次查找,看是否包含该元组,如果已经包含则替换旧值,查找过程类似于getEntry()
方法;如果没有找到则会在红黑树中插入新的entry
,如果插入之后破坏了红黑树的约束条件,还需要进行调整(旋转,改变某些节点的颜色).
1 | public V put(K key, V value) { |
上述代码的插入部分并不难理解:首先在红黑树上找到合适的位置,然后创建新的entry
并插入(当然,新插入的节点一定是树的叶子).难点是调整函数fixAfterInsertion()
,前面已经说过,调整往往需要1.改变某些节点的颜色,2.对某些节点进行旋转。
在介绍该方法之前,我们先来学习一下关于红黑树的插入操作:
红黑树的插入:
首先,插入的结点默认为红色.为何?可以从两个方面来解释:一,由性质d我们知道红黑树中黑节点至少是红节点的两倍,因此插入节点的父节点为黑色的概率较大,而此时并不需要作任何调整,因此效率较高;二,由性质e我们可以知道,当我们插入红色结点时,破坏的性质少,性质e并没有被破坏.
插入过程:
1 | 1.父为黑 |
现在我们来看看fixAfterInsertion()
的源码:
1 | private void fixAfterInsertion(TreeMap.Entry<K, V> x) { |
2.3.3 remove()
remove(Object key)
的作用是删除key
值对应的entry
,该方法首先通过上文中提到的getEntry(Object key)
方法找到key
值对应的entry
,然后调用deleteEntry(Entry<K,V> entry)
删除对应的entry
.由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束条件,因此有可能要进行调整.
getEntry()
函数上面已经讲解过,这里重点放deleteEntry()
上,该函数删除指定的entry
并在红黑树的约束被破坏时进行调用fixAfterDeletion(Entry<K,V> x)
进行调整.
我们先来看看红黑树的删除操作.
红黑树的删除:
由于红黑树是一棵改良版的二叉查找树,红黑树的删除操作跟普通二叉查找树的删除操作也就非常相似,唯一的区别是红黑树在节点删除之后可能需要进行调整.现在考虑一棵普通二叉查找树的删除过程,可以简单分为两种情况:
- 删除点p的左右子树都为空,或者只有一棵子树非空。
- 删除点p的左右子树都非空。
对于上述"情况1"处理起来比较简单,直接将p删除(左右子树都为空时),或者用非空子树替代p(只有一棵子树非空时);
对于"情况2"可以用p的后继s(树中大于x的最小的那个元素)代替p,然后使用情况1删除s(此时s一定满足情况1.可以画画看).
我们来看看deleteEntry()
源码:
1 | private void deleteEntry(TreeMap.Entry<K, V> p) { |
当结点被删除后,红黑树需要考虑是否需要调整.我们发现,如果删除的是红色结点,并不会破坏红黑树的性质.只有删除点是黑结点的时候,才会触发调整函数,
跟上文中讲过的fixAfterInsertion()
函数一样,fixAfterDeletion(Entry<K,V> x)
也要分成若干种情况.记住,无论有多少情况,具体的调整操作只有两种:1.改变某些节点的颜色,2.对某些节点进行旋转.
删除黑结点后的调整情况.
1 | 我们先假设: |
我们现在来看看fixAfterDeletion()
源码,大致分类情况相同,有些细节进行了优化.
1 | private void fixAfterDeletion(TreeMap.Entry<K, V> x) { |
我们分析代码可以知道,如果进入循环中的情况一,将情况1首先转换成情况2,或者转换成情况3和情况4。当然,并不意味着调整过程一定是从情况1开始.通过后续代码我们还会发现几个有趣的规则:
a).如果是由情况1之后紧接着进入的情况2,那么情况2之后一定会退出循环(因为x为红色);
b).一旦进入情况3和情况4,一定会退出循环(因为x为root).
c).情况3其实是落在情况4内的.
d).情况5~情况8跟前四种情况是对称的.
参考资料:
https://github.com/CarpenterLee/JCFInternals/blob/master/markdown/5-TreeSet%20and%20TreeMap.md