最近,本人在学习数据结构与算法的过程中,发现红黑树是一种特别有趣又比较难以理解的数据结构,再加上JDK1.8中,HashMap在满足一定条件(链表长度大于8,并且桶的长度大于64)之后,链表结构会自动转化成红黑树,所以,我觉得对于红黑树的总结十分必要。在拜读了一些大神的文章以后,本人以自己的理解来说说红黑树这件事,如有不对之处希望大神们可以多多指出。
在这里,感谢以下文章的参考:
红黑树(一)之 原理和算法详细介绍
史上最清晰的红黑树讲解(上)
30张图带你彻底理解红黑树
以及在线生成红黑树的网站:
红黑树生成网站
话不多说,我们进入正题吧。
1. 红黑树的前生(二叉查找树)
在数据结构中,二叉树是一种十分重要的数据结构,我们先看一下二叉树的定义:二叉树是每个结点最多有两个子树的树结构。 就是说树上的每一个结点,都具有5种形态,分别是该结点为空,该结点的子结点为空,该结点仅有左子节点,该结点仅有右子节点,该结点既有左子节点,又有右子节点。如图1所示,图中A表示该结点,B表示左子节点,C表示右子节点,每个结点都按照这个规律以此类推就形成了二叉树,图2是一个二叉树。
在了解了二叉树之后,什么是查找二叉树呢?查找二叉树是在二叉树的结构之上,对于二叉树的每个结点的值做出了限制,这里以数字为例子,就是每个结点的值(如果存在)要大于它的左子结点值(如果存在),小于它的右子结点值(如果存在),对应图1中,就是 B<A<C。这样的话,对于树的中序遍历将会出来的值将会有顺序,或者说树的结构符合了二分查找法。需要查询时,从根结点(对应图2的50)开始,依次将该结点值与待查询值比较,待查询值小,就往左查询,大就往右,以此类推,直到查到带查询值或者查到空值为止。
以图2的树为例子,我们查询分别查询30和58两个值。查询30时,先比较根结点50,30比50小,所以我们比较50的左子节点35,30比35小,继续比较35的左子结点22,30比22大,比较22的右子结点,右子结点为空,查询结束,该树中,没有30这个值。
查询58时,先比较根结点50,58比50大,所以我们比较50的右子节点60,58比60小,继续比较60的左子节点58,58与58相等,则查询结束,该查询成功。
由此可见,一般意义上,二叉查找树的平均查找长度要比线性表短很多,即查找更加方便。为什么说一般意义上,因为,比如我们将图2中的22,35,53,58结点删除,这个二叉查找树退化成了线性表,如图3所示。这是一种极端的情况,我们也可以理解为一般的查找二叉树在增删改时不够稳定,对应的算法复杂度也随着不断变化。我们希望增删改查的复杂度在一定程度上尽可能的低。所以,需要对二叉查找树进行限制,即为何需要红黑树。
2. 红黑树的介绍
红黑树是一种自平衡二叉查找树。它对于二叉树有了“颜色”上的限制,我们可以看下它的五点性质:
- 树中的结点是红色或黑色。
- 根结点是黑色。
- 空结点(NIL)为叶子结点,并且为黑色。
- 红结点的子结点一定是黑色结点。
- 从一个结点到该结点的叶子结点的所有路径上包含相同数目的黑结点。
看上去是不是特别复杂,我们现在看图来理解一下:
从上图,我们可以看出来,整个数里面只有黑色和红色的结点,根结点50是黑色的,叶子结点一定是空结点(NIL), 红结点55的下面是54和58两个黑色结点。同时任何结点到它对应的空结点的路径中黑色的数量一定相同的。
这里我们不急着往下讲,我们先观察这个树,总结一些它的规律,这些规律将对我们后续的操作有很大的帮助。
如果空结点的父结点是红色结点时,那么空结点的兄弟结点一定也是空结点。通俗点说,红结点下面一旦出现空结点就是两个,不会出现黑+空,或者红+空的情况。如果是黑+空,那么违反了性质5,如果出现红+空,那么违反了性质4.
如果空结点的父结点是黑结点时,那么空结点的兄弟结点一定不是黑结点。一样的道理,如果出现了黑+空,就会违反性质5。
总结一下,红黑树最底层只会出现两种情况,一种是底层两个都是空结点,另外一个是父结点为黑结点,子结点一个红色,一个为空,并且该红结点的两个子结点都是空。知道这一点有什么用呢?这个对于我们做增加删除时的理解将有很大帮助。
3. 红黑树的查找与增加结点
红黑树还是一种二叉查找树,所以查找方式依然不变,只需要按照上面的规则进行查找就行了。 并且它的时间复杂度永远保持在O(lgn),这个感兴趣的可以在我的参考文章中看下是怎么证明的,这里就不再进行叙述。这里我们重点讲解它的查找以及删除,这也是大家对于红黑树最头疼的地方。
在增加结点前需要先明确两个概念,左旋以及右旋。在这里我以左旋为例子,比如你就是一个要被左旋的结点,左旋实际上就是把的你右子结点变成你的父结点,同时把你右子结点的左子树变成你的右子树。如下面的图所示,50为旋转结点,当左旋发生后,他的右子结点55变成了父节点,而50变成了55的左子结点,并且原先55的左子树53,54给到50了,称为50的右子树。左右旋操作之后依旧满足二叉查找树的要求。
这里打个比方,比如你本来是个皇帝,你的右丞相谋朝篡位当上了皇帝,这时候,你成为了你原来右丞相的左丞相,而你右丞相本来左下方的那些人都给你管了。我们从图上可以直观的看出来,左旋发生前,感觉左边半边的点比较少,发生之后,结点开始跑向了左半边,整个树的结构更趋于平衡,实际上红黑树就是根据不断的变色,左旋和右旋来保证增删之后树的结构趋于平衡,这里,我不打算画右旋,因为上面这张图从右往左看就是右旋。下面,我们就来进行最难的两个部分,增加结点和删除结点了。
增加结点实际上可以看作先对该结点进行查找,如果存在,就把该结点的值更新,如果不存在,就在查找到最后返回时的空结点位置处增加该结点。对于红黑树而言,我们需要在增加完结点后,对这个树的结构进行一些调整,使树满足红黑树条件。
首先,为了算法的简便,我们默认插入的是红结点,因为,红结点的增加,不会改变树中的黑色结点数,即不会影响性质5。现在考虑插入的情况,从我们上节中总结的规律可以看出,在树的最底层只有3种情况,其中父亲结点为黑时,你可以在下面直接插入红结点,不需要做出任何改变。因为黑色结点下面是可以有红色的,不违背红黑树的任意一条规则。所以,只有一种情况需要进行结构调整,那就是父节点为红,并且此时插入结点的兄弟结点为空。
接下来,我们需要看看叔叔结点(父结点的兄弟结点) 的颜色了。第一类:当叔叔结点颜色为黑时,会有如下四种情况,这里我们用S表示插入结点,F表示插入结点的父亲结点,G表示爷爷结点,U表示叔叔结点:
在上图中,情况3 和4 是1和2 的镜像,我们这里说说情况1和2。
对于情况1,父节点为左子节点,插入结点也是左子结点,则把父结点变黑色,爷爷结点变红色,然后对爷爷结点做右旋,如下图所示,这样两个红色结点就不连在一起了,这时候认为结构调整结束。
对于情况2,我们只需要做一点小小改动就可以变成情况1,对父结点F进行左旋,左旋后,按照情况1处理。
情况3和4与1和2是对称的,只是操作中左旋变成右旋,右旋变成左旋而已。
下面讨论第二大类,就是叔叔结点是红色结点。这种情况比较简单。只用把F和U都变成黑色,而G变成红色。然后以G为插入结点,重新进行判断,此后,不外乎又是分成立G的父亲结点是红还是黑?G的父亲结点是红的情况下,G的叔叔结点是红还是黑?如此往复,直到结构调整结束或者根结点变红,如果根结点变成红色,则把根结点变黑,调整结束。在TreeMap的源码中,这个操作本身就是一个while的过程,感兴趣的可以去看下这段源码。
综上,我们的增加结点操作就结束了。
4. 红黑树的删除结点
对于删除操作,实际上就是找到待删除结点的后继结点,然后把后继结点删除,并把后继结点的值给到删除结点。那么后继结点怎么找呢?寻找中后继节点可能会出现两种情况:一种情况是删除结点有右子结点,那么寻找右子结点的最靠左的子节点,另外一种情况是删除结点没有右子结点。
现在我们先讨论没有右子结点的情况,当右子节点不存在时,根据之前的分析,只有一种情况,就是我们之前讨论出来的,该结点为黑结点,同时左子结点为红色结点并且红色结点下面为空,这时候,我们不需要寻找后续结点,直接删除该结点,并把下面的红色子结点变为黑色即可。
另外一种是有右子结点的情况,这是我们相当于删除右子结点最左边的非空子结点。
当删除结点为红色时,不需要讨论,直接删除,因为不会影响红黑树的性质。
删除为黑色时,就又要分情况进行讨论:
1.兄弟结点为红结点:
2.兄弟结点为黑结点,并且兄弟结点的子结点都是黑色
3.兄弟结点为黑结点,其子结点有红色结点。
1.兄弟结点为红结点:此时将兄弟结点变黑,父亲结点变红,然后对父亲结点做左旋操作,(第一次操作是CL.CR是空集合),此时,结点S的兄弟结点为CL,CL的为空,子结点也为空,即CL和其子结点都是黑色,这时,相当于变成了情况2,后续按照情况2处理。
2.兄弟结点为黑结点,并且兄弟结点的子结点都是黑色:此时,F结点红黑都有可能,这里用白色表示。将兄弟结点B变成红色,以F作为参考点,重新进行结构变换,这时不外乎又会出现以上情况,F结点是否为红?为红就变成黑色(实际删除的点是S点,所以这时候的参考点为红,只需要做变色处理即可) F的兄弟结点是否为红?F的兄弟结点为黑时,其兄弟的子结点为红还是黑?依次类推,直到结构变换结束,或者算到根节点结束。
3.兄弟结点为黑结点,其子结点有红色结点。
此时又可以分为两种情况,分别是CL为红色;CR为红色;CL,CR都为红色时按照CR为红色处理。
先说CR为红色的情况:将红色的CR和F都变成黑色,再将B变成F对应的颜色,之后对F进行左旋操作。正如我们之前所说,最底层时CL一定是空,而后面的操作又是自平衡过程,所以CL在这里只能是空或者红色。此时删除掉S,整个树结构符合红黑树特性。
CL为红色时:此时,将红色的CL设为黑色,B设为红色,并对B进行右旋操作,操作之后,CL变成了兄弟结点且为黑,其右子结点为红的情况,这样就与上一副图的情况一致,可以采用上一副图的操作,继续进行结构变化,依次类推,直到调整结束。
当删除结点为父结点的右子结点时,实际上是镜像的操作,在这里就不一一列举了。
这是本人第一篇博客,写的不好,请多包涵,如有不对之处,也欢迎指出,谢谢。