文章已同步发表于微信公众号JasonGaoH,我画了近百张图来理解红黑树,文章略有修改。html
以前在公司组内分享了红黑树的工做原理,今天把它整理下发出来,但愿能对你们有所帮助,对本身也算是一个知识点的总结。java
这篇文章算是我写博客写公众号以来画图最多的一篇文章了,没有之一,我但愿尽量多地用图片来形象地描述红黑树的各类操做的先后变换原理,帮助你们来理解红黑树的工做原理,下面,多图预警开始了。git
在讲红黑树以前,咱们首先来了解下下面几个概念:二叉树,排序二叉树以及平衡二叉树。github
二叉树指的是每一个节点最多只能有两个字数的有序树。一般左边的子树称为左子树
,右边的子树称为右子树
。这里说的有序树强调的是二叉树的左子树和右子树的次序不能随意颠倒。bash
二叉树简单的示意图以下:微信
代码定义:网络
class Node {
T data;
Node left;
Node right;
}
复制代码
所谓排序二叉树,顾名思义,排序二叉树是有顺序的,它是一种特殊结构的二叉树,咱们能够对树中全部节点进行排序和检索。性能
性质ui
排序二叉树简单示意图:spa
排序二叉树的左子树上全部节点的值小于根节点的值,右子树上全部节点的值大于根节点的值,当咱们插入一组元素正好是有序的时候,这时会让排序二叉树退化成链表。
正常状况下,排序二叉树是以下图这样的:
可是,当插入的一组元素正好是有序的时候,排序二叉树就变成了下边这样了,就变成了普通的链表结构,以下图所示:
正常状况下的排序二叉树检索效率相似于二分查找,二分查找的时间复杂度为 O(log n),可是若是排序二叉树退化成链表结构,那么检索效率就变成了线性的 O(n) 的,这样相对于 O(log n) 来讲,检索效率确定是要差很多的。
思考,二分查找和正常的排序二叉树的时间复杂度都是 O(log n),那么为何是O(log n) ?
关于 O(log n) 的分析下面这篇文章讲解的很是好,感兴趣的能够看下这篇文章 二分查找的时间复杂度,文章是拿二分查找来举例的,二分查找和平衡二叉树的时间复杂度是同样的,理解了二分查找的时间复杂度,再来理解平衡二叉树就不难了,这里就不赘述了。
继续回到咱们的主题上,为了解决排序二叉树在特殊状况下会退化成链表的问题(链表的检索效率是 O(n) 相对正常二叉树来讲要差很多),因此有人发明了平衡二叉树
和红黑树
相似的平衡树。
平衡二叉数又被称为 AVL 树,AVL 树的名字来源于它的发明做者 G.M. Adelson-Velsky 和 E.M. Landis,取自两人名字的首字母。
官方定义:它或者是一颗空树,或者具备如下性质的排序二叉树:它的左子树和右子树的深度之差(平衡因子)的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。
两个条件:
讲了这么多概念,接下来主角红黑树终于要上场了。
为何有红黑树?
其实红黑树和上面的平衡二叉树相似,本质上都是为了解决排序二叉树在极端状况下退化成链表致使检索效率大大下降的问题,红黑树最先是由 Rudolf Bayer 于 1972 年发明的。
红黑树首先确定是一个排序二叉树,它在每一个节点上增长了一个存储位来表示节点的颜色,能够是 RED 或 BLACK 。
Java 中实现红黑树大概结构图以下所示:
针对上面的 5 种性质,咱们简单理解下,对于性质 1 和性质 2 ,至关因而对红黑树每一个节点的约束,根节点是黑色,其余的节点要么是红色,要么是黑色。
对于性质 3 中指定红黑树的每一个叶子节点都是空节点,并且叶子节点都是黑色,但 Java 实现的红黑树会使用 null 来表明空节点,所以咱们在遍历 Java里的红黑树的时候会看不到叶子节点,而看到的是每一个叶子节点都是红色的,这一点须要注意。
对于性质 5,这里咱们须要注意的是,这里的描述是从任一节点,从任一节点到它的子树的每一个叶子节点黑色节点的数量都是相同的,这个数量被称为这个节点的黑高。
若是咱们从根节点出发到每一个叶子节点的路径都包含相同数量的黑色节点,这个黑色节点的数量被称为树的黑色高度。树的黑色高度和节点的黑色高度是不同的,这里要注意区分。
其实到这里有人可能会问了,红黑树的性质说了一大堆,那是否是说只要保证红黑树的节点是红黑交替就能保证树是平衡的呢?
其实不是这样的,咱们能够看来看下面这张图:
左边的子树都是黑色节点,可是这个红黑树依然是平衡的,5 条性质它都知足。
这个树的黑色高度为 3,从根节点到叶子节点的最短路径长度是 2,该路径上全是黑色节点,包括叶子节点,从根节点到叶子节点最长路径为 4,每一个黑色节点之间会插入红色节点。
经过上面的性质 4 和性质 5,其实上保证了没有任何一条路径会比其余路径长出两倍,因此这样的红黑树是平衡的。
其实这算是一个推论,红黑树在最差状况下,最长的路径都不会比最短的路径长出两倍。其实红黑树并非真正的平衡二叉树,它只能保证大体是平衡的,由于红黑树的高度不会无限增高,在实际应用用,红黑树的统计性能要高于平衡二叉树,但极端性能略差。
想要完全理解红黑树,除了上面说到的理解红黑树的性质之外,就是理解红黑树的插入操做了。
红黑树的插入和普通排序二叉树的插入基本一致,排序二叉树的要求是左子树上的全部节点都要比根节点小,右子树上的全部节点都要比跟节点大,当插入一个新的节点的时候,首先要找到当前要插入的节点适合放在排序二叉树哪一个位置,而后插入当前节点便可。红黑树和排序二叉树不一样的是,红黑树须要在插入节点调整树的结构来让树保持平衡。
通常状况下,红黑树中新插入的节点都是红色的,那么,为何说新加入到红黑树中的节点要是红色的呢?
这个问题能够这样理解,咱们从性质5中知道,当前红黑树中从根节点到每一个叶子节点的黑色节点数量是同样的,此时假如新的黑色节点的话,必然破坏规则,但加入红色节点却不必定,除非其父节点就是红色节点,所以加入红色节点,破坏规则的可能性小一些。
接下来咱们重点来说红黑树插入新节点后是如何保持平衡的。
给定下面这样一颗红黑树:
当咱们插入值为66的节点的时候,示意图以下:
很明显,这个时候结构依然遵循着上述5大特性,无需启动自动平衡机制调整节点平衡状态。
若是再向里面插入值为51的节点呢,这个时候红黑树变成了这样。
这样的结构其实是不知足性质4的,红色两个子节点必须是黑色的,而这里49这个红色节点如今有个51的红色节点与其相连。
这个时候咱们须要调整这个树的结构来保证红黑树的平衡。
首先尝试将49这个节点设置为黑色,以下示意图。
这个时候咱们发现黑高是不对的,其中 60-56-45-49-51-null 这条路径有 4 个黑节点,其余路径的黑色节点是 3 个。
接着调整红黑树,咱们再次尝试把45这个节点设置为红色的,以下图所示:
这个时候咱们发现问题又来了,56-45-43 都是红色节点的,出现了红色节点相连的问题。
因而咱们须要再把 56 和 43 设置为黑色的,以下图所示。
因而咱们把 68 这个红色节点设置为黑色的。
对于这种红黑树插入节点的状况下,咱们能够只须要经过变色就能够保持树的平衡了。可是并非每次都是这么幸运的,当变色行不通的时候,咱们须要考虑另外一个手段就是旋转了。
例以下面这种状况,一样仍是拿这颗红黑树举例。
如今这颗红黑树,咱们如今插入节点65。
咱们尝试把 66 这个节点设置为黑色,以下图所示。
这样操做以后黑高又出现不一致的状况了,60-68-64-null 有 3 个黑色节点,而60-68-64-66-null 这条路径有 4 个黑色节点,这样的结构是不平衡的。
或者咱们把 68 设置为黑色,把 64 设置为红色,以下图所示:
可是,一样的问题,上面这颗红黑树的黑色高度仍是不一致,60-68-64-null 和 60-68-64-66-null 这两条路径黑色高度仍是不一致。
这种状况若是只经过变色的状况是不能保持红黑树的平衡的。
接下来咱们讲讲红黑树的旋转,旋转分为左旋和右旋。
文字描述:逆时针旋转两个节点,让一个节点被其右子节点取代,而该节点成为右子节点的左子节点。
文字描述太抽象,接下来看下图片展现。
首先断开节点PL与右子节点G的关系,同时将其右子节点的引用指向节点C2;而后断开节点G与左子节点C2的关系,同时将G的左子节点的应用指向节点PL。
接下来再放下 gif 图,但愿能帮助你们更好地理解左旋,图片来自网络。
文字描述:顺时针旋转两个节点,让一个节点被其左子节点取代,而该节点成为左子节点的右子节点。
右旋的图片展现:
首先断开节点G与左子节点PL的关系,同时将其左子节点的引用指向节点C2;而后断开节点PL与右子节点C2的关系,同时将PL的右子节点的应用指向节点G。
右旋的gif展现(图片来自网络):
介绍完了左旋和右旋基本操做,咱们来详细介绍下红黑树的几种旋转场景。
以下图所示的红黑树,咱们插入节点是65。
操做步骤以下能够围绕祖父节点 69 右旋,再结合变色,步骤以下所示:
仍是上面这颗红黑树,咱们再插入节点 67。
这种状况咱们能够这样操做,先围绕父节点 66 左旋,而后再围绕祖父节点 69 右旋,最后再将 67 设置为黑色,把 69 设置为红色,以下图所示。
以下图这种状况,咱们要插入节点68。
这种状况,咱们能够先围绕父节点 69 右旋,接着再围绕祖父节点 66 左旋,最后把 68 节点设置为黑色,把 66 设置为红色,咱们的具体操做步骤以下所示。
仍是来上面的图来举例,咱们在这颗红黑树上插入节点 70 。
咱们能够这样操做围绕祖父节点 66 左旋,再把旋转后的根节点 69 设置为黑色,把 66 这个节点设置为红色。具体能够参看下图:
Java 中的红黑树实现类是 TreeMap ,接下来咱们尝试从源码角度来逐行解释 TreeMap 这一套机制是如何运做的。
// TreeMap中使用Entry来描述每一个节点
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;
...
}
复制代码
TreeMap 的put方法。
public V put(K key, V value) {
//先以t保存链表的root节点
Entry<K,V> t = root;
//若是t=null,代表是一个空链表,即该TreeMap里没有任何Entry做为root
if (t == null) {
compare(key, key); // type (and possibly null) check
//将新的key-value建立一个Entry,并将该Entry做为root
root = new Entry<>(key, value, null);
size = 1;
//记录修改次数加1
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
//若是比较器cpr不为null,即代表采用定制排序
if (cpr != null) {
do {
//使用parent上次循环后的t所引用的Entry
parent = t;
//将新插入的key和t的key进行比较
cmp = cpr.compare(key, t.key);
//若是新插入的key小于t的key,t等于t的左边节点
if (cmp < 0)
t = t.left;
//若是新插入的key大于t的key,t等于t的右边节点
else if (cmp > 0)
t = t.right;
else
//若是两个key相等,新value覆盖原有的value,并返回原有的value
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);
}
//将新插入的节点做为parent节点的子节点
Entry<K,V> e = new Entry<>(key, value, parent);
//若是新插入key小于parent的key,则e做为parent的左子节点
if (cmp < 0)
parent.left = e;
//若是新插入key小于parent的key,则e做为parent的右子节点
else
parent.right = e;
//修复红黑树
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
复制代码
//插入节点后修复红黑树
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
//直到x节点的父节点不是根,且x的父节点是红色
while (x != null && x != root && x.parent.color == RED) {
//若是x的父节点是其父节点的左子节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//获取x的父节点的兄弟节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//若是x的父节点的兄弟节点是红色
if (colorOf(y) == RED) {
//将x的父节点设置为黑色
setColor(parentOf(x), BLACK);
//将x的父节点的兄弟节点设置为黑色
setColor(y, BLACK);
//将x的父节点的父节点设为红色
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
}
//若是x的父节点的兄弟节点是黑色
else {
//TODO 对应状况第二种,左右节点旋转
//若是x是其父节点的右子节点
if (x == rightOf(parentOf(x))) {
//将x的父节点设为x
x = parentOf(x);
//右旋转
rotateLeft(x);
}
//把x的父节点设置为黑色
setColor(parentOf(x), BLACK);
//把x的父节点父节点设为红色
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
}
//若是x的父节点是其父节点的右子节点
else {
//获取x的父节点的兄弟节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//只着色的状况对应的是最开始例子,没有旋转操做,可是要对应屡次变换
//若是x的父节点的兄弟节点是红色
if (colorOf(y) == RED) {
//将x的父节点设置为黑色
setColor(parentOf(x), BLACK);
//将x的父节点的兄弟节点设为黑色
setColor(y, BLACK);
//将X的父节点的父节点(G)设置红色
setColor(parentOf(parentOf(x)), RED);
//将x设为x的父节点的节点
x = parentOf(parentOf(x));
}
//若是x的父节点的兄弟节点是黑色
else {
//若是x是其父节点的左子节点
if (x == leftOf(parentOf(x))) {
//将x的父节点设为x
x = parentOf(x);
//右旋转
rotateRight(x);
}
//将x的父节点设为黑色
setColor(parentOf(x), BLACK);
//把x的父节点的父节点设为红色
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//将根节点强制设置为黑色
root.color = BLACK;
}
复制代码
TreeMap的插入节点和普通的排序二叉树没啥区别,惟一不一样的是,在TreeMap 插入节点后会调用方法fixAfterInsertion(e)来从新调整红黑树的结构来让红黑树保持平衡。
咱们重点关注下红黑树的fixAfterInsertion(e)方法,接下来咱们来分别介绍两种场景来演示fixAfterInsertion(e)方法的执行流程。
一样是拿这颗红黑树举例,如今咱们插入节点 51。
当咱们须要插入节点51的时候,这个时候TreeMap 的 put 方法执行后会获得下面这张图。
接着调用fixAfterInsertion(e)方法,以下代码流程所示。
当第一次进入循环后,执行后会获得下面的红黑树结构。
在把 x 从新赋值后,从新进入 while 循环,此时的 x 节点为 45 。
执行上述流程后,获得下面所示的红黑树结构。
这个时候x被从新赋值为60,由于60是根节点,因此会退出 while 循环。在退出循序后,会再次把根节点设置为黑色,获得最终的结构以下图所示。
最后通过两次执行while循环后,咱们的红黑树会调整成如今这样的结构,这样的红黑树结构是平衡的,因此路径的黑高一致,而且没有红色节点相连的状况。
接下来咱们再来演示第二种场景,须要结合变色和旋转一块儿来保持平衡。
给定下面这样一颗红黑树:
如今咱们插入节点66,获得以下树结构。
一样地,咱们进入fixAfterInsertion(e)方法。
最终咱们获得的红黑树结构以下图所示:
调整成这样的结构咱们的红黑树又再次保持平衡了。
演示 TreeMap 的流程就拿这两种场景举例了,其余的就不一一举例了。
由于以前的分享只整理了红黑树的插入部分,原本想着红黑树的删除就不整理了,有人跟我反馈说红黑树的删除相对更复杂,因而索性仍是把红黑树的删除再整理下。
删除相对插入来讲,的确是要复杂一点,可是复杂的地方是由于在删除节点的这个操做状况有不少种,可是插入不同,插入节点的时候实际上这个节点的位置是肯定的,在节点插入成功后只须要调整红黑树的平衡就能够了。
可是删除不同的是,删除节点的时候咱们不能简单地把这个节点设置为null,由于若是这个节点有子节点的状况下,不能简单地把当前删除的节点设置为null,这个被删除的节点的位置须要有新的节点来填补。这样一来,须要分多种状况来处理了。
直接删除根节点便可。
直接删除当前节点便可。
这个时候须要使用子节点来代替当前须要删除的节点,而后再把子节点删除便可。
给定下面这棵树,当咱们须要删除节点69的时候。
首先用子节点代替当前待删除节点,而后再把子节点删除。
最终的红黑树结构以下面所示,这个结构的红黑树咱们是不须要经过变色+旋转来保持红黑树的平衡了,由于将子节点删除后树已是平衡的了。
还有一种场景是当咱们待删除节点是黑色的,黑色的节点被删除后,树的黑高就会出现不一致的状况,这个时候就须要从新调整结构。
仍是拿上面这颗删除节点后的红黑树举例,咱们如今须要删除节点67。
由于67 这个节点的两个子节点都是null,因此直接删除,获得以下图所示结构:
这个时候咱们树的黑高是不一致的,左边黑高是3,右边是2,因此咱们须要把64节点设置为红色来保持平衡。
删除节点两个子节点都不为空的状况下,跟上面有一个节点不为空的状况下也是有点相似,一样是须要找能替代当前节点的节点,找到后,把能替代删除节点值复制过来,而后再把替代节点删除掉。
那么什么叫作前驱,什么叫作后继呢? 前驱是左子树中最大的节点,后继则是右子树中最小的节点。
前驱或者后继都是最接近当前节点的节点,当咱们须要删除当前节点的时候,也就是找到能替代当前节点的节点,可以替代当前节点确定是最接近当前节点。
在当前删除节点两个子节点不为空的场景下,咱们须要再进行细分,主要分为如下三种状况。
以下面这样一棵树,咱们须要删除节点64:
首先找到前驱节点,把前驱节点复制到当前节点:
接着删除前驱节点。
这个时候63和60这个节点都是红色的,咱们尝试把60这个节点设置为红色便可使整个红黑树达到平衡。
前驱节点是黑色的,子节点都为空,这个时候操做步骤与上面基本相似。
以下操做步骤:
由于要删除节点64,接着找到前驱节点63,把63节点复制到当前位置,而后将前驱节点63删除掉,变色后出现黑高不一致的状况下,最后把63节点设置为黑色,把65节点设置为红色,这样就能保证红黑树的平衡。
给定下面这颗红黑树,咱们须要删除节点64的时候。
一样地,咱们找到64的前驱节点63,接着把63赋值到64这个位置。
而后删除前驱节点。
删除节点后不须要变色也不须要旋转便可保持树的平衡。
终于把红黑树的基本原理部分写完了,用了不少示意图,这篇文章是在以前分享的 ppt 上再整理出来,我以为本身应该算是把基本操做讲明白了,整理这篇文章前先后后用了近一周左右,由于平时上班,基本上只有周末有时间才有时间整理,若有问题请留言讨论。
若是您以为写得还能够,请您帮忙点个赞,您的点赞真的是对我最大的支持,也是我能继续写下去的动力,感谢。
文章中不少参考了下面文章的一些示意图,很是感谢如下文章。