红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明,在当时被称为对称二叉 B 树(symmetric binary B-trees)
。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改成现在的红黑树
。红黑树具备良好的效率,它可在 O(logN)
时间内完成查找、增长、删除等操做。所以,红黑树在业界应用很普遍,好比 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。考虑到红黑树是一种被普遍应用的数据结构,因此咱们颇有必要去弄懂它。html
学过二叉查找树的同窗都知道,普通的二叉查找树在极端状况下可退化成链表,此时的增删查效率都会比较低下。为了不这种状况,就出现了一些自平衡的查找树,好比 AVL,红黑树等。这些自平衡的查找树经过定义一些性质,将任意节点的左右子树高度差控制在规定范围内,以达到平衡状态。以红黑树为例,红黑树经过以下的性质定义实现自平衡:java
- 节点是红色或黑色。
- 根是黑色。
- 全部叶子都是黑色(叶子是NIL节点)。
- 每一个红色节点必须有两个黑色的子节点。(从每一个叶子到根的全部路径上不能有两个连续的红色节点。)
- 从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点(简称黑高)。
有了上面的几个性质做为限制,便可避免二叉查找树退化成单链表的状况。可是,仅仅避免这种状况还不够,这里还要考虑某个节点到其每一个叶子节点路径长度的问题。若是某些路径长度过长,那么,在对这些路径上的及诶单进行增删查操做时,效率也会大大下降。这个时候性质4和性质5用途就凸显了,有了这两个性质做为约束,便可保证任意节点到其每一个叶子节点路径最长不会超过最短路径的2倍。缘由以下:git
当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每一个叶子节点的全部路径必须包含相同数量的黑色节点。此时,在路径最长的状况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。举例说明一下,请看下图:github
上图画出了从根节点 M 出发的到其叶子节点的最长和最短路径。这里偷懒只画出了两条最长路径,实际上最长路径有4条,分别为:算法
M -> Q -> O -> N
数据结构
M -> Q -> O -> p
学习
M -> Q -> Y -> X
网站
M -> Q -> Y -> Z
ui
长度为4,最短路径为 M -> E
,长度为2。最长路径的长度正好为最短路径长度的2倍。spa
前面说了关于红黑树的一些性质,这里还须要补充一些其余方面的东西。在红黑树简介一节中说到红黑树被发明出来的时候并不叫红黑树
,而是叫作对称二叉 B 树
,从名字中可发现红黑树和 B 树(这里指的是2-3树)或许有必定的关联,事实也正是如此。若是对红黑树的性质稍加修改,就能让红黑树和B树造成一一对应的关系。关于红黑树和 B 树关系的细节这里不展开说明了,有兴趣的同窗能够参考《算法》第4版,那本书上讲的很透彻。
红黑树的基本操做和其余树形结构同样,通常都包括查找、插入、删除等操做。前面说到,红黑树是一种自平衡的二叉查找树,既然是二叉查找树的一种,那么查找过程和二叉查找树同样,比较简单,这里再也不赘述。相对于查找操做,红黑树的插入和删除操做就要复杂的多。尤为是删除操做,要处理的状况比较多,不过你们若是静下心来去看,会发现其实也没想的那么难。好了,废话就说到这,接下来步入正题吧。
在分析插入和删除操做前,这里须要插个队,先说明一下旋转操做,这个操做在后续操做中都会用获得。旋转操做分为左旋和右旋,左旋是将某个节点旋转为其右孩子的左孩子,而右旋是节点旋转为其左孩子的右孩子。这话听起来有点绕,因此仍是请看下图:
上图包含了左旋和右旋的示意图,这里以右旋为例进行说明,右旋节点 M 的步骤以下:
上面分析了右旋操做,左旋操做与此相似,你们有兴趣本身画图试试吧,这里再也不赘述了。旋转操做自己并不复杂,这里先分析到这吧。
红黑树的插入过程和二叉查找树插入过程基本相似,不一样的地方在于,红黑树插入新节点后,须要进行调整,以知足红黑树的性质。性质1规定红黑树节点的颜色要么是红色要么是黑色,那么在插入新节点时,这个节点应该是红色仍是黑色呢?答案是红色,缘由也不难理解。若是插入的节点是黑色,那么这个节点所在路径比其余路径多出一个黑色节点,这个调整起来会比较麻烦(参考红黑树的删除操做,就知道为啥多一个或少一个黑色节点时,调整起来这么麻烦了)。若是插入的节点是红色,此时全部路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的状况。这种状况下,经过变色和旋转进行调整便可,比以前的简单多了。
接下来,将分析插入红色节点后红黑树的状况。这里假设要插入的节点为 N,N 的父节点为 P,祖父节点为 G,叔叔节点为 U。插入红色节点后,会出现5种状况,分别以下:
插入的新节点 N 是红黑树的根节点,这种状况下,咱们把节点 N 的颜色由红色变为黑色,性质2(根是黑色)被知足。同时 N 被染成黑色后,红黑树全部路径上的黑色节点数量增长一个,性质5(从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点)仍然被知足。
N 的父节点是黑色,这种状况下,性质4(每一个红色节点必须有两个黑色的子节点)和性质5没有受到影响,不须要调整。
N 的父节点是红色(节点 P 为红色,其父节点必然为黑色),叔叔节点 U 也是红色。因为 P 和 N 均为红色,全部性质4被打破,此时须要进行调整。这种状况下,先将 P 和 U 的颜色染成黑色,再将 G 的颜色染成红色。此时通过 G 的路径上的黑色节点数量不变,性质5仍然知足。但须要注意的是 G 被染成红色后,可能会和它的父节点造成连续的红色节点,此时须要递归向上调整。
N 的父节点为红色,叔叔节点为黑色。节点 N 是 P 的右孩子,且节点 P 是 G 的左孩子。此时先对节点 P 进行左旋,调整 N 与 P 的位置。接下来按照状况五进行处理,以恢复性质4。
N 的父节点为红色,叔叔节点为黑色。N 是 P 的左孩子,且节点 P 是 G 的左孩子。此时对 G 进行右旋,调整 P 和 G 的位置,并互换颜色。通过这样的调整后,性质4被恢复,同时也未破坏性质5。
上面五种状况中,状况一和状况二比较简单,状况3、4、五稍复杂。但若是细心观察,会发现这三种状况的区别在于叔叔节点的颜色,若是叔叔节点为红色,直接变色便可。若是叔叔节点为黑色,则须要选选择,再交换颜色。当把这三种状况的图画在一块儿就区别就比较容易观察了,以下图:
相较于插入操做,红黑树的删除操做则要更为复杂一些。删除操做首先要肯定待删除节点有几个孩子,若是有两个孩子,不能直接删除该节点。而是要先找到该节点的前驱(该节点左子树中最大的节点)或者后继(该节点右子树中最小的节点),而后将前驱或者后继的值复制到要删除的节点中,最后再将前驱或后继删除。因为前驱和后继至多只有一个孩子节点,这样咱们就把原来要删除的节点有两个孩子的问题转化为只有一个孩子节点的问题,问题被简化了一些。咱们并不关心最终被删除的节点是不是咱们开始想要删除的那个节点,只要节点里的值最终被删除就好了,至于树结构如何变化,这个并不重要。
红黑树删除操做的复杂度在于删除节点的颜色,当删除的节点是红色时,直接拿其孩子节点补空位便可。由于删除红色节点,性质5(从任一节点到其每一个叶子的全部简单路径都包含相同数目的黑色节点)仍可以被知足。当删除的节点是黑色时,那么全部通过该节点的路径上的黑节点数量少了一个,破坏了性质5。若是该节点的孩子为红色,直接拿孩子节点替换被删除的节点,并将孩子节点染成黑色,便可恢复性质5。但若是孩子节点为黑色,处理起来就要复杂的多。分为6种状况,下面会展开说明。
在展开说明以前,咱们先作一些假设,方便说明。这里假设最终被删除的节点为X
(至多只有一个孩子节点),其孩子节点为N
,X
的兄弟节点为S
,S
的左节点为 SL,右节点为 SR。接下来讨论是创建在节点 X
被删除,节点 N
替换X
的基础上进行的。这里说明把被删除的节点X
特意拎出来讲一下的缘由是防止你们误觉得节点N
会被删除,否则后面就会看不明白。
在上面的基础上,接下来就能够展开讨论了。红黑树删除有6种状况,分别是:
N 是新的根。在这种情形下,咱们就作完了。咱们从全部路径去除了一个黑色节点,而新根是黑色的,因此性质都保持着。
上面是维基百科中关于红黑树删除的状况一说明,因为没有配图,看的有点晕。通过思考,我以为可能会是下面这种情形:
要删除的节点 X 是根节点,且左右孩子节点均为空节点,此时将节点 X 用空节点替换完成删除操做。
可能还有其余情形,你们若是知道,烦请告知。
S 为红色,其余节点为黑色。这种状况下能够对 N 的父节点进行左旋操做,而后互换 P 与 S 颜色。但这并未结束,通过节点 P 和 N 的路径删除前有3个黑色节点(P -> X -> N
),如今只剩两个了(P -> N
)。比未通过 N 的路径少一个黑色节点,性质5仍不知足,还须要继续调整。不过此时能够按照状况4、5、六进行调整。
N 的父节点,兄弟节点 S 和 S 的孩子节点均为黑色。这种状况下能够简单的把 S 染成红色,全部通过 S 的路径比以前少了一个黑色节点,这样通过 N 的路径和通过 S 的路径黑色节点数量一致了。但通过 P 的路径比不通过 P 的路径少一个黑色节点,此时须要从状况一开始对 P 进行平衡处理。
N 的父节点为红色,叔叔节点为黑色。节点 N 是 P 的右孩子,且节点 P 是 G 的左孩子。此时先对节点 P 进行左旋,调整 N 与 P 的位置。接下来按照状况五进行处理,以恢复性质4。
这里须要特别说明一下,上图中的节点 N 并不是是新插入的节点。当 P 为红色时,P 有两个孩子节点,且孩子节点均为黑色,这样从 G 出发到各叶子节点路径上的黑色节点数量才能保持一致。既然 P 已经有两个孩子了,因此 N 不是新插入的节点。状况四是由以 N 为根节点的子树中插入了新节点,通过调整后,致使 N 被变为红色,进而致使了状况四的出现。考虑下面这种状况(PR 节点就是上图的 N 节点):
如上图,插入节点 N 并按状况三处理。此时 PR 被染成了红色,与 P 节点造成了连续的红色节点,这个时候就需按状况四再次进行调整。
S 为黑色,S 的左孩子为红色,右孩子为黑色。N 的父节点颜色可红可黑,且 N 是 P 左孩子。这种状况下对 S 进行右旋操做,并互换 S 和 SL 的颜色。此时,全部路径上的黑色数量仍然相等,N 兄弟节点的由 S 变为了 SL,而 SL 的右孩子变为红色。接下来咱们到状况六继续分析。
S 为黑色,S 的右孩子为红色。N 的父节点颜色可红可黑,且 N 是其父节点左孩子。这种状况下,咱们对 P 进行左旋操做,并互换 P 和 S 的颜色,并将 SR 变为黑色。由于 P 变为黑色,因此通过 N 的路径多了一个黑色节点,通过 N 的路径上的黑色节点与删除前的数量一致。对于不通过 N 的路径,则有如下两种状况:
红黑树删除的状况比较多,你们刚开始看的时候可能会比较晕。可能会产生这样的疑问,为啥红黑树会有这种删除状况,为啥又会有另外一种状况,它们之间有什么联系和区别?和你们同样,我刚开始看的时候也有这样的困惑,直到我把全部状况对应的图形画在一块儿时,拨云见日,一切都明了了。此时天空中出现了4个字,原来如此、原来如此、原来如此。因此,请看图吧:
红黑树是一种重要的二叉树,应用普遍,但在不少数据结构相关的书本中出现的次数并很少。不少书中要么不说,要么就一笔带过,并不会进行详细的分析,这多是由于红黑树比较复杂的缘故。我在学习红黑树的时候也找了不少资料,但整体感受讲的都不太好。尤为是在我学习删除操做的时候,不少资料是实在人看不下去,看的我很痛苦。直到我看到维基百科上关于红黑树的分析时,非常欣喜。这篇文章分析的颇有条理,言简意赅,比不少资料好了太多。本文对红黑树的分析也主要参考了维基百科中的红黑树分析,并对维基百科中容易让人产生疑问和误解的地方进行了说明。同时维基百科中文版红黑树文中的图片较为模糊,这里我从新进行了绘制。须要说明的是,维基百科中文版没法打开了,文中关于维基百科的连接都是英文版的。另外在给你们推荐一个数据结构可视化的网站,里面包含常见数据结构可视化过程,地址为:t.cn/RZFgryr。
另外,因为红黑树自己比较复杂,实现也较为复杂。在写这篇文章以前,我曾尝试过用 Java 语言实现红黑树的增删操做,最终只写出了新增节点操做,删除没作出来。并且本身写的新增逻辑实在太繁琐,写的很差看,无法拿出来 show。因此最后把 Java 中的 TreeMap 增删相关源码拷出来,按照本身的需求把源码修改了一下,也勉强算是实现了红黑树吧。代码放到了 github 上,传送门 -> RBTree.java。
最后,若是你也在学习红黑树,但愿这篇文章可以帮助到你。另外,因为红黑树自己比较复杂,加之本人水平有限,不免会出一些错误。若是有错,还望你们指出来,咱们共同讨论。