Java集合源码分析之基础(六):红黑树(RB Tree)

红黑树和AVL树的思想是相似的,都是在插入过程当中对二叉排序树进行调整,从而提高性能,它的增删改查都可以在**O(lg n)**内完成。git

本文会从定义到实现一棵红黑树展开,还会简单介绍其与AVL树的异同。github

定义

红黑树是一棵二叉排序树。且知足如下特色:编程

  1. 每一个节点或者是黑色,或者是红色。
  2. 根节点是黑色。
  3. 每一个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
  4. 若是一个节点是红色的,则它的两个儿子都是黑色的。
  5. 从一个节点到该节点的子孙节点的全部路径上包含相同数目的黑节点。

下图就是一棵简单的红黑树示例: 数组

红黑树示例

示例中每一个结点最后都是一个NIL结点,它是黑色的,不过咱们画图时一般会省略它。因此下文以及后续文章中绘制时都会省略NIL结点,你们记得还有它就能够。微信

实现原理

红黑树的插入与删除和AVL树相似,也是每插入一个结点,都检查是否破坏了树的结构,而后进行调整。红黑树每一个结点插入时默认都为红色,这样作能够下降黑高,也能够减小调整的次数。数据结构

插入元素

红黑树的概念理解起来较为复杂,咱们以一个简单的示例,看看如何构造一棵红黑树。源码分析

现有数组int[] a = {1, 10, 9, 2, 3, 8, 7, 4, 5, 6};咱们要将其变为一棵红黑树。性能

首先插入1,此时树是空的,1就是根结点,根结点是黑色的:3d

插入1

而后插入元素10,此时依然符合规则,结果以下:code

插入10

当插入元素9时,这时是须要调整的第一种状况,结果以下:

插入9

红黑树规则4中强调不能有两个相邻的红色结点,因此此时咱们须要对其进行调整。调整的原则有多个相关因素,这里的状况是,父结点10是其祖父结点1(父结点的父结点)的右孩子,当前结点9是其父结点10的左孩子,且没有叔叔结点(父结点的兄弟结点),此时须要进行两次旋转,第一次,以父结点10右旋:

右旋

而后将父结点**(此时是9)染为黑色,祖父结点1**染为红色,以下所示:

染色

而后以祖父结点1左旋:

左旋

下一步,插入元素2,结果以下:

插入2

此时状况与上一步相似,区别在于父结点1是祖父结点9的左孩子,当前结点2是父结点的右孩子,且叔叔结点10是红色的。这时须要先将叔叔结点10染为黑色,再进行下一步操做,具体作法是将父结点1和叔叔结点10染为黑色,祖父结点9染为红色,以下所示:

染色

因为结点9是根节点,必须为黑色,将它染为黑色便可:

染色

下一步,插入元素3,以下所示:

插入3

这和咱们以前插入元素10的状况如出一辙,须要将父结点2染为黑色,祖父结点1染为红色,以下所示:

染色

而后左旋:

左旋

下一步,插入元素8,结果以下:

插入8

此时和插入元素2有些相似,区别在于父结点3是右孩子,当前结点8也是右孩子,这时也须要先将叔叔结点1染为黑色,具体操做是先将13染为黑色,再将祖父结点2染为红色,以下所示:

染色

此时树已经平衡了,不须要再进行其余操做了,如今插入元素7,以下所示:

插入7

这时和以前插入元素9时如出一辙了,先将78右旋,以下所示:

右旋

而后将7染为黑色,3染为红色,再进行左旋,结果以下:

左旋

下一步要插入的元素是4,结果以下:

插入4

这里和插入元素2是相似的,先将38染为黑色,7染为红色,以下所示:

染色

但此时27相邻且颜色均为红色,咱们须要对它们继续进行调整。这时状况变为了父结点2为红色,叔叔结点10为黑色,且2为左孩子,7为右孩子,这时须要以2左旋。这时左旋与以前不一样的地方在于结点7旋转完成后将有三个孩子,结果相似于下图:

错误示意图

这种状况处理起来也很简单,只须要把7原来的左孩子3,变成2的右孩子便可,结果以下:

调整

而后再把2的父结点7染为黑色,祖父结点9染为红色。结果以下所示:

染色

此时又须要右旋了,咱们要以9右旋,右旋完成后7又有三个孩子,这种状况和上述是对称的,咱们把7原有的右孩子8,变成9的左孩子便可,以下所示:

右旋

下一个要插入的元素是5,插入后以下所示:

插入5

有了上述一些操做,处理5变得十分简单,将3染为红色,4染为黑色,而后左旋,结果以下所示:

左旋

最后插入元素6,以下所示:

插入6

又是叔叔结点3为红色的状况,这种状况咱们处理过屡次了,首先将35染为黑色,4染为红色,结果以下:

染色

此时问题向上传递到了元素4,咱们看2479的颜色和位置关系,这种状况咱们也处理过,先将29染为黑色,7染为红色,结果以下:

染色

最后7是根结点,染为黑色便可,最终结果以下所示:

最终结果

能够看到,在插入元素时,叔叔结点是主要影响因素,待插入结点与父结点的关系决定了是否须要屡次旋转。能够总结为如下几种状况:

  • 若是父结点是黑色,插入便可,无需调整。

  • 若是叔叔结点是红色,就把父结点和叔叔结点都转为黑色,祖父结点转为红色,将不平衡向上传递。

  • 若是叔叔结点是黑色或者没有叔叔结点,就看父结点和待插入结点的关系。若是待插入结点和父结点的关系,与父结点与祖父结点的关系一致,好比待插入结点是父结点的左孩子,父结点也是祖父结点的左孩子,就无需屡次旋转。不然就先经过相应的旋转将其关系变为一致。

删除元素

要从一棵红黑树中删除一个元素,主要分为三种状况。

状况1:待删除元素没有孩子

没有孩子指的是没有值不为NIL的孩子。这种状况下,若是删除的元素是红色的,能够直接删除,若是删除的元素是黑色的,就须要进行调整了。

例如咱们从下图中删除元素1:

红黑树

删除元素1后,2的左孩子为NIL,这条支路上的黑色结点数就比其余支路少了,因此须要进行调整。

这时,咱们的关注点从叔叔结点转到兄弟结点,也就是结点4,此时4是红色的,就把它染为黑色,把父结点2染为红色,以下所示:

染色

而后以2左旋,结果以下:

左旋

此时兄弟结点为3,且它没有红色的孩子,这时只须要把它染为红色,父结点2染为黑色便可。结果以下所示:

调整完毕

状况2:待删除元素有一个孩子

这应该是删除操做中最简单的一种状况了,根据红黑树的定义,咱们能够推测,若是一个元素仅有一个孩子,那么这个元素必定是黑色的,并且其孩子是红色的。

假设咱们有一个红色节点,它是树中的某一个节点,且仅有一个孩子,那么根据红色节点不能相邻的条件,它的孩子必定是黑色的,以下所示:

红色节点仅一个孩子

但这个子树的黑高却再也不平衡了(注意每一个节点的叶节点都是一个NIL节点),所以红色节点不可能只有一个孩子。

而如果一个黑色节点仅有一个孩子,若是其孩子是黑色的,一样会打破黑高的平衡,因此其孩子只能是红色的,以下所示:

黑色节点仅一个孩子

只有这一种状况符合红黑树的定义,这时要删除这个元素,只须要使用其孩子代替它,仅代替值而不代替颜色便可,上图的状况删除完后变为:

删除完毕

能够看到,树的黑高并无发生变化,所以也不须要进行调整。

状况3:待删除元素有两个孩子

咱们在讨论二叉排序树时说过,若是删除一个有两个孩子的元素,可使用它的前驱或者后继结点代替它。由于它的前驱或者后继结点最多只会有一个孩子,因此这种状况能够转为状况1或状况2处理。

总结

删除元素最复杂的是状况1,这主要由其兄弟结点以及兄弟结点的孩子颜色共同决定。这里简要作下总结。

咱们以N表明当前待删除节点,以P表明父结点,以S表明兄弟结点,以SL表明兄弟结点的左孩子,SR表明兄弟结点的右孩子,以下所示:

图样

根据红黑树定义,这种状况下S要么有红色的子结点,要么只有NIL结点,如下对S有黑色结点的状况均表示NIL

主要有如下几种:

  1. S是红色,P必定是黑色,S也不会有红色的孩子,以下:

红色兄弟结点

此时把PS颜色变换,再左旋,以下:

左旋

这样变换后,N支路上的黑色结点并无增长,因此依然少一个,

  1. P,S以及S的所有孩子都是黑色

不管S有几个孩子,或者没有孩子,只要不是红色都是这种状况,此时状况以下:

全黑色

咱们把S染为红色,这样一来,NS两个支路都少了一个黑色结点,因此能够把问题向父结点转移,经过递归解决。染色后以下:

染色

  1. P为红(S必定为黑),S的孩子都为黑

这种状况最为简单,只须要把P和S颜色交换便可。这样N支路多了一个黑色元素,而S支路没有减小,因此达到了平衡。

交换前

交换后

  1. P任意色,S为黑,N是P的左孩子,S的右孩子SR为红,S的左孩子任意

以下所示

任意色

此时将S改成P的颜色,SRP改成黑色,而后左旋,结果以下:

左旋

能够发现,此时N支路多了一个黑色结点,而其他支路均没有收到影响,因此调整完毕。

  1. P任意色,S为黑,N是P的左孩子,S的左孩子SL为红,S的右孩子SR为黑,以下所示:
    SR黑色

此时变换SSL的颜色,而后右旋,结果以下:

右旋

这时,全部分支的黑色结点数均没有改变,但状况5转为了状况4,再进行一次操做便可。

还有一些状况与上述是对称的,咱们进行相应的转换便可。

#总结 红黑树的操做比较复杂,插入元素可能须要屡次变色与旋转,删除也是。这些操做的目的都是为了保证红黑树的结构不被破坏。这些复杂的插入与删除操做但愿你们能够亲手尝试一下,以加深理解。

红黑树是JDK中TreeMapTreeSet的底层数据结构,在JDK1.8中HashMap也用到了红黑树,因此掌握它对咱们后续的分析十分重要。

关于红黑树与AVL树的区别,以及为什么选用红黑树,已经不属于咱们的讨论范围,你们能够查阅相关资料进一步了解。

上一篇:Java集合源码分析之基础(五):平衡二叉树(AVL Tree)

下一篇:Java集合源码分析之Iterable概述

本文到此就结束了,若是您喜欢个人文章,能够关注个人微信公众号: 大大纸飞机

或者扫描下方二维码直接添加:

公众号

您也能够关注个人github:github.com/LtLei/artic…

编程之路,道阻且长。惟,路漫漫其修远兮,吾将上下而求索。

相关文章
相关标签/搜索