说到HashMap,就一定要说到红黑树,红黑树作为一种自平衡二叉查找树,是一种用途较广的数据结构,在jdk1.8中使用红黑树提升HashMap的性能,今天就来说一说红黑树。

前言

限于篇幅,本文只对红黑树的基础进行说明,暂不涉及源码部分,大部分摘抄自维基百科,这里也贴出对应链接:

维基百科(中文):https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91

维基百科:https://en.wikipedia.org/wiki/Red%E2%80%93black_tree

笔者这里会根据维基百科的讲解做些说明,方便初学者理解。

定义

当然,在了解红黑树之前,先回顾下树这种结构的特点(来自维基百科):

在计算机科学中,树(英语:tree)是一种抽象数据类型(ADT)或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

每个节点都只有有限个子节点或无子节点;
没有父节点的节点称为根节点;
每一个非根节点有且只有一个父节点;
除了根节点外,每个子节点可以分为多个不相交的子树;
树里面没有环路(cycle)

红黑树(英语:Red–black tree)是一种自平衡二叉查找树,红黑树和AVL树一样都对插入时间、删除时间和查找时间提供了最好可能的最坏情况担保。
红黑树的结构复杂,但它的操作有着良好的最坏情况运行时间,并且在实践中高效:它可以在O(log n)时间内完成查找,插入和删除,这里的O(log n) n是树中元素的数目。

这些描述说明了红黑树结构的一大特点:在增删查上即使是最坏的一种数据结构情况,也保证了性能。


有些新手可能会想,为什么能保证?

看完整篇文章,你可能就会了解清楚,红黑树概念上其实也提及了,自平衡这个词说明了每次在我们进行增删时,会自行调整结构来满足红黑树特性,这样在查询上就能保证性能。


那么,什么样的二叉树才是红黑树呢?

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

1.节点是红色或黑色。

2.根是黑色。

3.所有叶子都是黑色(叶子是NIL节点)。

4.每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)

5.从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

这里需要注意叶子节点的概念与一般意义上树的叶子节点不同,这里红黑树叶子节点都是黑色且为NIL的。

维基百科上对于这5种性质也做了说明:

这些约束确保了红黑树的关键特性:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

要知道为什么这些性质确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点包含数据。用这种范例表示红黑树是可能的,但是这会改变一些性质并使算法复杂。为此,本文中我们使用"nil叶子"或"空(null)叶子",如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。

从根到叶子的最长的可能路径不多于最短的可能路径的两倍长这个在文中也解释了原因,因为性质4需要被满足,这样限制了树的高度差,使得查找性能提升。

可以多想下,普通的二叉树,最坏情况如下:

这种情况下使得树形结构毫无意义,而红黑树(其存在的5种特性保证)则不会出现这种情况。

树高度

具体请参考维基百科的证明:

自平衡操作

之前的定义我也说了,为了保证红黑树的特性,需要在插入删除时对树进行调整来保证自平衡。那么如何来调整呢?

这里就说明下红黑树平衡的两种操作:变色和旋转

变色

变色这个操作很好理解了吧,在某些情况下,为了保证自平衡,需要改变颜色,来满足红黑树的特性

旋转

可以参考维基百科:https://en.wikipedia.org/wiki/Tree_rotation

旋转操作相对要复杂些,旋转分为左旋和右旋。

  • 左旋:

如下图所示,逆时针旋转旋转M,N两个节点,使得父节点M变为N的子节点,N变为父节点,交换之后对于B节点就需要进行调整,B节点变为M的右子节点

  • 右旋:

如下图所示,顺时针旋转旋转M,N两个节点,使得父节点M变为N的子节点,N变为父节点,交换之后对于B节点就需要进行调整,B节点变为M的左子节点

这里贴出维基上的gif图,方便各位理解:

无论是变色还是旋转,最终调整的结果都是要在插入或者删除之后满足红黑树的5个特性。

插入调整

首先需要明白的是新增加的节点默认标记为红色,至于原因维基上说明了:

如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑节点,这个是很难调整的。但是设为红色节点后,可能会导致出现两个连续红色节点的冲突,那么可以通过颜色调换(color flips)和树旋转来调整。

在说我个人理解之前,需要理解局部红黑子树含义,其实相当于把红黑树进行切割,一个大的红黑树切割成多个局部,每次我们调整都是以一个局部来完成,局部调整完之后,整个树如果还是不平衡,则继续向上回溯调整,维基上也是这样做的。下图中的框算是一个局部红黑子树。

如果添加的节点默认标记为黑色,那么这条路径上比其他路径多了一个黑色节点,不满足性质5,需要调整,可能会影响到其他已经平衡的局部红黑子树,牵一发而动全身,代价过高,而默认为红色,首先其他已经平衡的局部红黑子树还未受到影响,在未调整之前,未平衡的这个局部红黑子树中黑色路径和平衡之前是相同的,这样应该是要更方便调整。

将要插入的节点标为N,N的父节点标为P,N的祖父节点标为G,N的叔父节点标为U

这里也能看出来都是以一个局部来调整,那么考虑下插入会出现哪些情况?在考虑的时候一定要注意,在未插入新节点和插入新节点之后变化的地方和需要保持不变的地方。尤其需要注意插入前已经平衡了,满足红黑树的5种特性,不是随便什么状态都会出现,切记。

插入会出现4种情况:

1.N为根节点,即红黑树的根节点。

2.N的父节点(P)是黑色的。

3.N的父节点(P)是红色的(因此它不能是树的根)而N的叔父节点(U)是红色的。

4.N的父节点(P)是红色的(因此它不能是树的根)而N的叔父节点(U)是黑色的。

思考下,上面包含的情况可以从插入节点N为红色开始进行考虑,判断父节点颜色,根据情况进行调整,先进行变色操作,变色保证不了平衡则需要旋转来平衡,而3和4的情况,有人可能会想为什么要判断U侧的子树部分?我们可以想一下,在P为红色,N为红色时,在N这一侧的子树部分,如何调整也不会达到平衡,此时只能向上回溯寻求帮助,涉及到了祖父节点G,那么此时就会影响到G的子树U部分,所以需要考虑U的颜色然后继续调整。

插入时按顺序判断上述4种情况,注意先对局部红黑子树(从N到G,G相当于局部根节点)进行平衡,满足属性4和5,最后判断G节点是否为黑色,非黑色以G为新增节点再次递归顺序判断(相当于向上回溯),这里有一个要注意的地方,局部根节点G可以为红色,除非G是整个树的根节点,这时直接修改颜色即可完成平衡(相当于整个树黑色路径都加一):

CASE-1:N为根节点,修改成黑色,同样满足红黑树的5个属性,仅仅黑色路径增加了1。不满足当前情况,继续CASE-2情况判断处理。

CASE-2:P为黑色,添加节点N为红色,N下添加两个黑色叶子节点(NIL),属性4和5没有破坏,不需要调整。不满足当前情况,继续CASE-3情况判断处理。

CASE-3:P(父)和U(叔)为红色,以N为P的左子节点为例(右子节点操作类似),添加节点N为红色,先进行变色操作,P,U变为黑色,这样不满足属性5,路径上黑色节点多了1,将G(祖父)变为红色,到这里可以看出添加节点之后从G到G下的叶子节点路径和未添加N节点之前的路径黑色节点数是一致的,在这部分局部区域已经满足属性4和5,不需要进行调整了,但是G可能违反了属性2(根节点为黑色),或属性4(不能有连续的两个红色节点),回到CASE-1再次顺序判断处理,这里注意下,重新判断是以G节点为新局部红黑子树的新增节点N,G节点的子节点可以当做叶子节点,再重新重头判断这个新的局部子树(相当于向上回溯,直到满足条件,或者根节点,满足CASE-1)。不满足当前情况,继续CASE-4情况判断处理。

CASE-4:P(父)为红色,U(叔)为黑色,以P为其父节点左子节点为例,对称情况操作类似,变色也满足不了G到叶子节点属性4和属性5,这里就需要考虑另一种处理-旋转。

这里会出现两种状态(对称的情况自行参照处理):

CASE-4-1:新添加节点N为P(父)节点右子树,相当于G树的“内部”,参考下图,在这种情况下,执行的P上的左旋转。转换成右侧图,到此还是违反属性4,但是不违反属性5,再继续CASE-4-2情况判断处理。不满足当前情况,也继续CASE-4-2情况判断处理。

CASE-4-2:新添加节点N为P(父)节点左子树,相当于G树的“外部”,和第一种状态调整完一致,参考下图,在这种情况下,执行G上的右旋转,P和G的颜色切换,即可满足红黑树的属性,变为右侧图。

从上面分析过程可以看出,局部插入最多两次旋转,更多的是变色操作,少量的旋转操作可以锁住某些节点(变色操作不影响),大部分节点是可以查询且修改的,这也是大部分底层实现使用红黑树的原因吧。

总结

删除操作比较复杂,下一章再进行说明,本章主要从基础说明红黑树的特性以及相关的平衡操作以及插入新节点时不同情况的调整规则,希望对读者有所帮助,下面通过流程图来帮助各位更好的理解和实现红黑树插入情况下的自平衡处理。

下一章我就更为复杂的删除操作进行讲解,谢谢各位阅读,如有错误,欢迎留言指正,我将尽快验证修改。

参考资料:

  1. https://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91
  2. https://en.wikipedia.org/wiki/Red%E2%80%93black_tree

JDK源码那些事儿之红黑树基础上篇的更多相关文章

  1. JDK源码那些事儿之红黑树基础下篇

    说到HashMap,就一定要说到红黑树,红黑树作为一种自平衡二叉查找树,是一种用途较广的数据结构,在jdk1.8中使用红黑树提升HashMap的性能,今天就来说一说红黑树,上一讲已经给出插入平衡的调整 ...

  2. JDK源码那些事儿之HashMap.TreeNode

    前面几篇文章已经讲解过HashMap内部实现以及红黑树的基础知识,今天这篇文章就讲解之前HashMap中未讲解的红黑树操作部分,如果没了解红黑树,请去阅读前面的两篇文章,能更好的理解本章所讲解的红黑树 ...

  3. JDK源码那些事儿之并发ConcurrentHashMap上篇

    前面已经说明了HashMap以及红黑树的一些基本知识,对JDK8的HashMap也有了一定的了解,本篇就开始看看并发包下的ConcurrentHashMap,说实话,还是比较复杂的,笔者在这里也不会过 ...

  4. JDK源码那些事儿之我眼中的HashMap

    源码部分从HashMap说起是因为笔者看了很多遍这个类的源码部分,同时感觉网上很多都是粗略的介绍,有些可能还不正确,最后只能自己看源码来验证理解,写下这篇文章一方面是为了促使自己能深入,另一方面也是给 ...

  5. JDK源码那些事儿之浅析Thread上篇

    JAVA中多线程的操作对于初学者而言是比较难理解的,其实联想到底层操作系统时我们可能会稍微明白些,对于程序而言最终都是硬件上运行二进制指令,然而,这些又太过底层,今天来看一下JAVA中的线程,浅析JD ...

  6. JDK源码那些事儿之常用的ArrayList

    前面已经讲解集合中的HashMap并且也对其中使用的红黑树结构做了对应的说明,这次就来看下简单一些的另一个集合类,也是日常经常使用到的ArrayList,整体来说,算是比较好理解的集合了,一起来看下 ...

  7. 浅析Java源码之HashMap外传-红黑树Treenode(已鸽)

    (这篇文章暂时鸽了,有点理解不能,点进来的小伙伴可以撤了) 刚开始准备在HashMap中直接把红黑树也过了的,结果发现这个类不是一般的麻烦,所以单独开一篇. 由于红黑树之前完全没接触过,所以这篇博客相 ...

  8. JDK源码那些事儿之并发ConcurrentHashMap下篇

    上一篇文章已经就ConcurrentHashMap进行了部分说明,介绍了其中涉及的常量和变量的含义,有些部分需要结合方法源码来理解,今天这篇文章就继续讲解并发ConcurrentHashMap 前言 ...

  9. JDK源码那些事儿之ConcurrentLinkedDeque

    非阻塞队列ConcurrentLinkedQueue我们已经了解过了,既然是Queue,那么是否有其双端队列实现呢?答案是肯定的,今天就继续说一说非阻塞双端队列实现ConcurrentLinkedDe ...

随机推荐

  1. css实现可伸缩的搜索框

    效果图: 代码: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" ...

  2. codevs1227:方格取数2

    题目描述 Description 给出一个n*n的矩阵,每一格有一个非负整数Aij,(Aij <= )现在从(,)出发,可以往右或者往下走,最后到达(n,n),每达到一格,把该格子的数取出来,该 ...

  3. python线程事件Event(30)

    在python项目开发中,线程thread使用是比较常见的,在前面的文章中我们介绍了 python线程的创建 以及 线程互斥锁 ,今天还要额外介绍一个与线程相关的内容 – 事件Event. 一.pyt ...

  4. Java使用icepdf转高清图片

    <dependency> <groupId>org.icepdf.os</groupId> <artifactId>icepdf-core</ar ...

  5. CCF201403 无线网络【限制型最短路】

    问题描述 目前在一个很大的平面房间里有 n 个无线路由器,每个无线路由器都固定在某个点上.任何两个无线路由器只要距离不超过 r 就能互相建立网络连接. 除此以外,另有 m 个可以摆放无线路由器的位置. ...

  6. 小程序使用mpvue框架无缝接入Vant Weapp组件库

    有美团开源出的mpvue以其vue的语法和良好的开发效率再搭配上用户体验良好的UI组件无疑是定制化微信小程序的开发方式,然而由于mpvue是对微信原生开发的再次封装,这也为我们引入UI组件添加了不少麻 ...

  7. c++学习总结(一)------类结构学习

    基类的构造函数并没有被派生类继承 析构函数和拷贝赋值操作符同样也没有 类的设计者通过把成员函数声明为 const 以表明它们不修改类对象 把一个修改类数据成员的函数声明为 const 是非法的 (51 ...

  8. VMware Workstation虚拟机安装

    VMware Workstation虚拟机安装 我们缺乏资金, 所以吃土.我们想搭 hadoop 集群, redis 集群, k8s 集群, 怎么办? 用虚拟机!! 一. 安装普通版本(Worksta ...

  9. 西门子软件sinutrain-for-sinumerik-operate-v4.8安装报错的处理

       西门子软件sinutrain-for-sinumerik-operate-v4.8安装报错的处理 安装时报错提示: 原因: 可能缺少某些文件 处理: 安装 海康ivms-4200后,可直接安装西 ...

  10. (十七)Hibnernate 和 Spring 整合

    一.Hibnernate 和 Spring结合方案: 方案一:  框架各自使用自己的配置文件,Spring中加载Hibernate的配置文件. 方案二:   统一由Spring的配置来管理.(推荐使用 ...