转载自:https://mp.weixin.qq.com/s/cnDx8lJ6fXHgLZWsqjWrag
一、二叉查找树(Binary Search Tree)
为什么要提到二叉查找树呢?红黑树是一种自平衡的二叉查找树
二叉查找树(BST)具备什么特性呢?
1.左子树上所有结点的值均小于或等于它的根结点的值。
2.右子树上所有结点的值均大于或等于它的根结点的值。
3.左、右子树也分别为二叉排序树。
如下图这棵树,就是一棵典型的二叉查找树:
比如查找值为10的节点,先找根节点,而后一层一层进行比较。你发现这是二分查找的思想啊,查找所需要的最大次数等同于二叉查找树的高度。不仅查找,在插入新节点的时候也是运用了同样的思路,通过一层一层比较大小,找到新节点适合插入的位置。在某种程序上,解决了很大问题。但二叉查找树仍然存在它的缺陷。经过多次插入操作后的树结构如下图:
可以看到,严重失衡了。这样的状态虽然也符合二叉查找树的特性,但是查找的性能大打折扣,几乎变成了线性查找。如何解决二叉查找树多次插入新节点导致的不平衡呢?我们的主角红黑树应运而生了。
二、红黑树(Red Black Tree)
1、普通的二叉查找树在极端情况下可退化成链表,此时的增删查效率都会比较低下。为了避免这种情况,就出现了一些自平衡的查找树,比如 AVL,红黑树等。这些自平衡的查找树通过定义一些性质,将任意节点的左右子树高度差控制在规定范围内,以达到平衡状态。
2、红黑树是一种特化的AVL树(平衡二叉树),在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。它可以在O(log n)时间内做查找,插入和删除。因此,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。
3、在红黑树上只读操作不需要对用于二叉查找树的操作做出修改,因为它也是二叉查找树。但是,在插入和删除之后,红黑属性可能变得违规。
4、如果插入的节点是黑色,那么这个节点所在路径比其他路径多出一个黑色节点,这个调整起来会比较麻烦(参考红黑树的删除操作,就知道为啥多一个或少一个黑色节点时,调整起来这么麻烦了)。如果插入的节点是红色,此时所有路径上的黑色节点数量不变,仅可能会出现两个连续的红色节点的情况。这种情况下,通过变色和旋转进行调整即可,比之前的简单多了。
5、如果删除的节点是红色,则不做任何操作,红黑树的任何属性都不会被破坏;如果删除的节点是黑色的,显然它所在的路径上就少一个黑色节点,红黑树的性质就被破坏了。
6、Nil为叶子结点,并且它是黑色的。(注意:在Java中,叶子结点是为null的结点)
红黑树是一种自平衡的二叉查找树。除了符合二叉查找树的基本特性外,它还具有下列附加特性:
1.结点是红色或黑色。
2.根结点是黑色。
3.每个叶子结点都是黑色的空结点(NIL结点)。
4 每个红色结点的两个子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
5.从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点。
下图中这棵树,就是一颗典型的红黑树:
正是由于这些规则限制,才保证了红黑树自平衡的关键性质:红黑树从根到叶子的最长路径不会超过最短路径的2倍。
有了上面的几个性质作为限制,即可避免二叉查找树退化成单链表的情况。但是,仅仅避免这种情况还不够,这里还要考虑某个节点到其每个叶子节点路径长度的问题。如果某些路径长度过长,那么,在对这些路径上的及诶单进行增删查操作时,效率也会大大降低。这个时候性质4和性质5用途就凸显了,有了这两个性质作为约束,即可保证任意节点到其每个叶子节点路径最长不会超过最短路径的2倍。原因如下: https://segmentfault.com/a/1190000012728513
当某条路径最短时,这条路径必然都是由黑色节点构成。当某条路径长度最长时,这条路径必然是由红色和黑色节点相间构成(性质4限定了不能出现两个连续的红色节点)。而性质5又限定了从任一节点到其每个叶子节点的所有路径必须包含相同数量的黑色节点。此时,在路径最长的情况下,路径上红色节点数量 = 黑色节点数量。该路径长度为两倍黑色节点数量,也就是最短路径长度的2倍。举例说明一下,请看下图:
上图画出了从根节点 M 出发的到其叶子节点的最长和最短路径。这里偷懒只画出了两条最长路径,实际上最长路径有4条,分别为:
M -> Q -> O -> N
M -> Q -> O -> p
M -> Q -> Y -> X
M -> Q -> Y -> Z
长度为4,最短路径为 M -> E,长度为2。最长路径的长度正好为最短路度的2倍。
对于性质5的不易理解之处:
2.1、红黑树的查找
因为红黑树是一颗二叉平衡树,并且查找不会破坏树的平衡,所以查找跟二叉平衡树的查找无异:
从根结点开始查找,把根结点设置为当前结点;
若当前结点为空,返回null;
若当前结点不为空,用当前结点的key跟查找key作比较;
若当前结点key等于查找key,那么该key就是查找目标,返回当前结点;
若当前结点key大于查找key,把当前结点的左子结点设置为当前结点,重复步骤2;
若当前结点key小于查找key,把当前结点的右子结点设置为当前结点,重复步骤2;
2.2、红黑树的修改
当插入或删除节点的时候,红黑树的规则有可能被打破。这时候就需要做出一些调整,从而继续维持我们的规则。
调整的方法有2种:变色和旋转。旋转又包含两种方式:左旋转和右旋转。
(一)变色
仅仅把一个节点变色是不行的,会导致相关路径凭空多出一个黑色结点,这样就打破了规则5。我们还需要对其他节点进行一些操作。
(二)左旋转
逆时针旋转红黑树的两个结点,使得父结点被自己的右孩子取代,而自己成为自己的左孩子。说起来很怪异,大家看下图:
(三)右旋转
顺时针旋转红黑树的两个结点,使得父结点被自己的左孩子取代,而自己成为自己的右孩子。大家看下图:
具体如何调整呢,在红黑树插入新节点的时候,可以分为5种不同的局面,每一种局面有不同的调整方法,下面详细列举:
局面1:新节点A位于树根,没有父节点---直接把新节点变色为黑色即可
局面2:新节点B的父节点是黑色---这种局面新插入的红色节点B没有打破红黑树的规则,不需要做任何调整
局面3:新节点D的父节点和叔叔节点都是红色---需要调整
①、违反规则4
②、将节点B变成黑色。这样一来,结点B所在路径凭空多了一个黑色结点,打破了规则5
③、让结点A变为红色,这时候,结点A和C又成为了连续的红色结点,违反规则4
④、再让结点C变为黑色,完成。注意:A为红色后,可能会和它的父节点形成连续的红色节点,此时需要递归向上调整。
局面4:新节点D的父节点是红色,叔叔节点是黑色或者没有叔叔节点。且新节点是父节点的右孩子,父节点B是祖父节点的左孩子。---需要调整
①、我们以父节点B为轴,做一次左旋转,使得新节点D成为父节点,原来的父节点B成为D的左孩子。这样一来,我们进入局面5
局面5:新节点D的父节点是红色,叔叔节点是黑色或者没有叔叔节点。且新节点是父节点的左孩子,父节点B是祖父节点的左孩子。---需要调整
①、我们以节点A为轴,做一次右旋转,使得新节点B成为父节点,原来的父节点A成为B的右孩子
②、让结点B变为黑色,结点A变为红色,完成
或许有人会问,如果局面4和局面5当中的父结点B是祖父结点A的右孩子该怎么办呢?
很简单,如果局面4中的父结点B是右孩子,则成为了局面5的镜像,原本的右旋操作改为左旋;如果局面5中的父结点B是右孩子,则成为了局面4的镜像,原本的左旋操作改为右旋。
总结:
局面3:条件:父节点B为红色,叔叔节点为红色 --> 直接三次变色。如果还有需要,递归向上调整即可
局面4和5:
条件:父节点B为红色,叔叔节点为黑色或没有叔叔节点
情况①:新节点D是父节点B的右孩子,父节点B是祖父节点A的左孩子 --> 以父节点B为轴左旋转,进入情况②处理
情况②:新节点D是父节点B的左孩子,父节点B是祖父节点A的左孩子 --> 以祖父节点A为轴右旋转,变色B黑A红,完成
情况③:新节点D是父节点B的左孩子,父节点B是祖父节点A的右孩子 --> 以父节点B为轴右旋转,进入情况④处理
情况④:新节点D是父节点B的右孩子,父节点B是祖父节点A的右孩子 --> 以祖父节点A为轴左旋转,变色B黑A红,完成
三、红黑树插入实践
①插入节点21,先看看满足以上哪种情况。满足局面3 “新结点的父结点和叔叔结点都是红色”,我们进行变色处理。
②经过三次变色,22变黑,25变红,27变黑
③经过上面的调整,以结点25为根的子树符合了红黑树规则,但结点25和结点17成为了连续的红色结点,违背规则4。
于是,我们把结点25看做一个新结点,正好符合局面5的镜像:
“新结点的父结点是红色,叔叔结点是黑色或者没有叔叔,且新结点是父结点的右孩子,父结点是祖父结点的右孩子”
于是我们以根结点13为轴进行左旋转,使得结点17成为了新的根结点。
④接下来,让结点17变为黑色,结点13变为红色。如此一来,我们的红黑树变得重新符合规则。