红黑树和AVL树的思想是相似的,都是在插入过程当中对二叉排序树进行调整,从而提高性能,它的增删改查都可以在**O(lg n)**内完成。git
本文会从定义到实现一棵红黑树展开,还会简单介绍其与AVL树的异同。github
红黑树是一棵二叉排序树。且知足如下特色:编程
下图就是一棵简单的红黑树示例: 数组
示例中每一个结点最后都是一个NIL结点,它是黑色的,不过咱们画图时一般会省略它。因此下文以及后续文章中绘制时都会省略NIL结点,你们记得还有它就能够。微信
红黑树的插入与删除和AVL树相似,也是每插入一个结点,都检查是否破坏了树的结构,而后进行调整。红黑树每一个结点插入时默认都为红色,这样作能够下降黑高,也能够减小调整的次数。数据结构
红黑树的概念理解起来较为复杂,咱们以一个简单的示例,看看如何构造一棵红黑树。源码分析
现有数组int[] a = {1, 10, 9, 2, 3, 8, 7, 4, 5, 6};
咱们要将其变为一棵红黑树。性能
首先插入1,此时树是空的,1就是根结点,根结点是黑色的:3d
而后插入元素10,此时依然符合规则,结果以下:code
当插入元素9时,这时是须要调整的第一种状况,结果以下:
红黑树规则4中强调不能有两个相邻的红色结点,因此此时咱们须要对其进行调整。调整的原则有多个相关因素,这里的状况是,父结点10是其祖父结点1(父结点的父结点)的右孩子,当前结点9是其父结点10的左孩子,且没有叔叔结点(父结点的兄弟结点),此时须要进行两次旋转,第一次,以父结点10右旋:
而后将父结点**(此时是9)染为黑色,祖父结点1**染为红色,以下所示:
而后以祖父结点1左旋:
下一步,插入元素2,结果以下:
此时状况与上一步相似,区别在于父结点1是祖父结点9的左孩子,当前结点2是父结点的右孩子,且叔叔结点10是红色的。这时须要先将叔叔结点10染为黑色,再进行下一步操做,具体作法是将父结点1和叔叔结点10染为黑色,祖父结点9染为红色,以下所示:
因为结点9是根节点,必须为黑色,将它染为黑色便可:
下一步,插入元素3,以下所示:
这和咱们以前插入元素10的状况如出一辙,须要将父结点2染为黑色,祖父结点1染为红色,以下所示:
而后左旋:
下一步,插入元素8,结果以下:
此时和插入元素2有些相似,区别在于父结点3是右孩子,当前结点8也是右孩子,这时也须要先将叔叔结点1染为黑色,具体操做是先将1和3染为黑色,再将祖父结点2染为红色,以下所示:
此时树已经平衡了,不须要再进行其余操做了,如今插入元素7,以下所示:
这时和以前插入元素9时如出一辙了,先将7和8右旋,以下所示:
而后将7染为黑色,3染为红色,再进行左旋,结果以下:
下一步要插入的元素是4,结果以下:
这里和插入元素2是相似的,先将3和8染为黑色,7染为红色,以下所示:
但此时2和7相邻且颜色均为红色,咱们须要对它们继续进行调整。这时状况变为了父结点2为红色,叔叔结点10为黑色,且2为左孩子,7为右孩子,这时须要以2左旋。这时左旋与以前不一样的地方在于结点7旋转完成后将有三个孩子,结果相似于下图:
这种状况处理起来也很简单,只须要把7原来的左孩子3,变成2的右孩子便可,结果以下:
而后再把2的父结点7染为黑色,祖父结点9染为红色。结果以下所示:
此时又须要右旋了,咱们要以9右旋,右旋完成后7又有三个孩子,这种状况和上述是对称的,咱们把7原有的右孩子8,变成9的左孩子便可,以下所示:
下一个要插入的元素是5,插入后以下所示:
有了上述一些操做,处理5变得十分简单,将3染为红色,4染为黑色,而后左旋,结果以下所示:
最后插入元素6,以下所示:
又是叔叔结点3为红色的状况,这种状况咱们处理过屡次了,首先将3和5染为黑色,4染为红色,结果以下:
此时问题向上传递到了元素4,咱们看2、4、7、9的颜色和位置关系,这种状况咱们也处理过,先将2和9染为黑色,7染为红色,结果以下:
最后7是根结点,染为黑色便可,最终结果以下所示:
能够看到,在插入元素时,叔叔结点是主要影响因素,待插入结点与父结点的关系决定了是否须要屡次旋转。能够总结为如下几种状况:
若是父结点是黑色,插入便可,无需调整。
若是叔叔结点是红色,就把父结点和叔叔结点都转为黑色,祖父结点转为红色,将不平衡向上传递。
若是叔叔结点是黑色或者没有叔叔结点,就看父结点和待插入结点的关系。若是待插入结点和父结点的关系,与父结点与祖父结点的关系一致,好比待插入结点是父结点的左孩子,父结点也是祖父结点的左孩子,就无需屡次旋转。不然就先经过相应的旋转将其关系变为一致。
要从一棵红黑树中删除一个元素,主要分为三种状况。
没有孩子指的是没有值不为NIL的孩子。这种状况下,若是删除的元素是红色的,能够直接删除,若是删除的元素是黑色的,就须要进行调整了。
例如咱们从下图中删除元素1:
删除元素1后,2的左孩子为NIL,这条支路上的黑色结点数就比其余支路少了,因此须要进行调整。
这时,咱们的关注点从叔叔结点转到兄弟结点,也就是结点4,此时4是红色的,就把它染为黑色,把父结点2染为红色,以下所示:
而后以2左旋,结果以下:
此时兄弟结点为3,且它没有红色的孩子,这时只须要把它染为红色,父结点2染为黑色便可。结果以下所示:
这应该是删除操做中最简单的一种状况了,根据红黑树的定义,咱们能够推测,若是一个元素仅有一个孩子,那么这个元素必定是黑色的,并且其孩子是红色的。
假设咱们有一个红色节点,它是树中的某一个节点,且仅有一个孩子,那么根据红色节点不能相邻的条件,它的孩子必定是黑色的,以下所示:
但这个子树的黑高却再也不平衡了(注意每一个节点的叶节点都是一个NIL节点),所以红色节点不可能只有一个孩子。
而如果一个黑色节点仅有一个孩子,若是其孩子是黑色的,一样会打破黑高的平衡,因此其孩子只能是红色的,以下所示:
只有这一种状况符合红黑树的定义,这时要删除这个元素,只须要使用其孩子代替它,仅代替值而不代替颜色便可,上图的状况删除完后变为:
能够看到,树的黑高并无发生变化,所以也不须要进行调整。
咱们在讨论二叉排序树时说过,若是删除一个有两个孩子的元素,可使用它的前驱或者后继结点代替它。由于它的前驱或者后继结点最多只会有一个孩子,因此这种状况能够转为状况1或状况2处理。
删除元素最复杂的是状况1,这主要由其兄弟结点以及兄弟结点的孩子颜色共同决定。这里简要作下总结。
咱们以N表明当前待删除节点,以P表明父结点,以S表明兄弟结点,以SL表明兄弟结点的左孩子,SR表明兄弟结点的右孩子,以下所示:
根据红黑树定义,这种状况下S要么有红色的子结点,要么只有NIL结点,如下对S有黑色结点的状况均表示NIL
主要有如下几种:
此时把P和S颜色变换,再左旋,以下:
这样变换后,N支路上的黑色结点并无增长,因此依然少一个,
不管S有几个孩子,或者没有孩子,只要不是红色都是这种状况,此时状况以下:
咱们把S染为红色,这样一来,N和S两个支路都少了一个黑色结点,因此能够把问题向父结点转移,经过递归解决。染色后以下:
这种状况最为简单,只须要把P和S颜色交换便可。这样N支路多了一个黑色元素,而S支路没有减小,因此达到了平衡。
以下所示
此时将S改成P的颜色,SR和P改成黑色,而后左旋,结果以下:
能够发现,此时N支路多了一个黑色结点,而其他支路均没有收到影响,因此调整完毕。
此时变换S和SL的颜色,而后右旋,结果以下:
这时,全部分支的黑色结点数均没有改变,但状况5转为了状况4,再进行一次操做便可。
还有一些状况与上述是对称的,咱们进行相应的转换便可。
#总结 红黑树的操做比较复杂,插入元素可能须要屡次变色与旋转,删除也是。这些操做的目的都是为了保证红黑树的结构不被破坏。这些复杂的插入与删除操做但愿你们能够亲手尝试一下,以加深理解。
红黑树是JDK中TreeMap、TreeSet的底层数据结构,在JDK1.8中HashMap也用到了红黑树,因此掌握它对咱们后续的分析十分重要。
关于红黑树与AVL树的区别,以及为什么选用红黑树,已经不属于咱们的讨论范围,你们能够查阅相关资料进一步了解。
上一篇:Java集合源码分析之基础(五):平衡二叉树(AVL Tree)
本文到此就结束了,若是您喜欢个人文章,能够关注个人微信公众号: 大大纸飞机
或者扫描下方二维码直接添加:
您也能够关注个人github:github.com/LtLei/artic…
编程之路,道阻且长。惟,路漫漫其修远兮,吾将上下而求索。