17张图带你解析红黑树的原理!保证你能看懂!

二叉查找树

因为红黑树本质上就是一棵二叉查找树,因此在了解红黑树以前,我们先来看下二叉查找树。
二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具备下列性质的二叉树:
  • 若任意结点的左子树不空,则左子树上全部结点的值均小于它的根结点的值;
  • 若任意结点的右子树不空,则右子树上全部结点的值均大于它的根结点的值;
  • 任意结点的左、右子树也分别为二叉查找树。
  • 没有键值相等的结点(no duplicate nodes)。
由于,一棵由n个结点,随机构造的二叉查找树的高度为lgn,因此瓜熟蒂落,通常操做的执行时间为O(lgn).(至于n个结点的二叉树高度为lgn的证实,可参考算法导论 第12章 二叉查找树 第12.4节)。
但二叉树若退化成了一棵具备n个结点的线性链后,则此些操做最坏状况运行时间为O(n)。后面咱们会看到一种基于二叉查找树-红黑树,它经过一些性质使得树相对平衡,使得最终查找、插入、删除的时间复杂度最坏状况下依然为O(lgn)。

红黑树

前面咱们已经说过,红黑树,本质上来讲就是一棵二叉查找树,但它在二叉查找树的基础上增长了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。
但它是如何保证一棵n个结点的红黑树的高度始终保持在h = logn的呢?这就引出了红黑树的5条性质:
1)每一个结点要么是红的,要么是黑的。
2)根结点是黑的。
3)每一个叶结点(叶结点即指树尾端NIL指针或NULL结点)是黑的。
4)若是一个结点是红的,那么它的俩个儿子都是黑的。
5)对于任一结点而言,其到叶结点树尾端NIL指针的每一条路径都包含相同数目的黑结点。
正是红黑树的这5条性质,使得一棵n个结点是红黑树始终保持了logn的高度,从而也就解释了上面咱们所说的“红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论的缘由。
以下图所示,便是一颗红黑树:

上文中咱们所说的 "叶结点" 或"NULL结点",它不包含数据而只充当树在此结束的指示,这些结点以及它们的父结点,在绘图中都会常常被省略。

树的旋转知识

当咱们在对红黑树进行插入和删除等操做时,对树作了修改,那么可能会违背红黑树的性质。
为了继续保持红黑树的性质,咱们能够经过对结点进行从新着色,以及对树进行相关的旋转操做,即修改树中某些结点的颜色及指针结构,来达到对红黑树进行插入或删除结点等操做后,继续保持它的性质或平衡。
树的旋转,分为左旋和右旋,如下借助图来作形象的解释和介绍:

1.左旋


如上图所示:
当在某个结点pivot上,作左旋操做时,咱们假设它的右孩子y不是NIL[T],pivot能够为任何不是NIL[T]的左孩子结点。
左旋以pivot到y之间的链为“支轴”进行,它使y成为该孩子树新的根,而y的左孩子b则成为pivot的右孩子。
左旋操做的参考代码以下所示(以x代替上述的pivot):
LEFT-ROTATE(T, x)  
1  y ← right[x] ▹ Set y.  
2  right[x] ← left[y]      ▹ Turn y's left subtree into x's right subtree.  
3  p[left[y]] ← x  
4  p[y] ← p[x]             ▹ Link x's parent to y. 5 if p[x] = nil[T] 6 then root[T] ← y 7 else if x = left[p[x]] 8 then left[p[x]] ← y 9 else right[p[x]] ← y 10 left[y] ← x ▹ Put x on y's left.  
11  p[x] ← y  复制代码

2.右旋

右旋与左旋差很少,再此不作详细介绍。

对于树的旋转,能保持不变的只有原树的搜索性质,而原树的红黑性质则不能保持,在红黑树的数据插入和删除后可利用旋转和颜色重涂来恢复树的红黑性质。

红黑树的插入

要真正理解红黑树的插入和删除,还得先理解二叉查找树的插入和删除。磨刀不误砍柴工,我们再来分别了解下二叉查找树的插入和删除。

二叉查找树的插入

若是要在二叉查找树中插入一个结点,首先要查找到结点插入的位置,而后进行插入,假设插入的结点为z的话,插入的伪代码以下:
TREE-INSERT(T, z)
 1  y ← NIL
 2  x ← root[T]
 3  while x ≠ NIL
 4      do y ←  x
 5         if key[z] < key[x]
 6            then x ← left[x]
 7            else x ← right[x]
 8  p[z] ← y
 9  if y = NIL
10     then root[T] ← z              ⊹ Tree T was empty
11     else if key[z] < key[y]
12             then left[y] ← z
13             else right[y] ← z复制代码
能够看到,上述第3-7行代码便是在二叉查找树中查找z待插入的位置,若是插入结点z小于当前遍历到的结点,则到当前结点的左子树中继续查找,若是z大于当前结点,则到当前结点的右子树中继续查找,第9-13行代码找到待插入的位置,若是z依然比此刻遍历到的新的当前结点小,则z做为当前结点的左孩子,不然做为当前结点的右孩子。

红黑树的插入和插入修复

如今咱们了解了二叉查找树的插入,接下来,我们便来具体了解红黑树的插入操做。红黑树的插入至关于在二叉查找树插入的基础上,为了从新恢复平衡,继续作了插入修复操做。
假设插入的结点为z,红黑树的插入伪代码具体以下所示:
RB-INSERT(T, z)  
 1  y ← nil[T]  
 2  x ← root[T]  
 3  while x ≠ nil[T]  
 4      do y ← x  
 5         if key[z] < key[x]  
 6            then x ← left[x]  
 7            else x ← right[x]  
 8  p[z] ← y  
 9  if y = nil[T]  
10     then root[T] ← z  
11     else if key[z] < key[y]  
12             then left[y] ← z  
13             else right[y] ← z  
14  left[z] ← nil[T]  
15  right[z] ← nil[T]  
16  color[z] ← RED  
17  RB-INSERT-FIXUP(T, z)  复制代码
咱们把上面这段红黑树的插入代码,跟咱们以前看到的二叉查找树的插入代码,能够看出,RB-INSERT(T, z)前面的第1-13行代码基本就是二叉查找树的插入代码,而后第14-16行代码把z的左孩子、右孩子都赋为叶结点nil,再把z结点着为红色,最后为保证红黑性质在插入操做后依然保持,调用一个辅助程序RB-INSERT-FIXUP来对结点进行从新着色,并旋转。
换言之
  • 若是插入的是根结点,由于原树是空树,此状况只会违反性质2,因此直接把此结点涂为黑色。
  • 若是插入的结点的父结点是黑色,因为此不会违反性质2和性质4,红黑树没有被破坏,因此此时也是什么也不作。
但当遇到下述3种状况时:
  • 插入修复状况1:若是当前结点的父结点是红色且祖父结点的另外一个子结点(叔叔结点)是红色
  • 插入修复状况2:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子
  • 插入修复状况3:当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子
又该如何调整呢?答案就是根据红黑树插入代码RB-INSERT(T, z)最后一行调用的RB-INSERT-FIXUP(T,z)所示操做进行,具体以下所示:
RB-INSERT-FIXUP(T,z)
 1 while color[p[z]] = RED  
 2     do if p[z] = left[p[p[z]]]  
 3           then y ← right[p[p[z]]]  
 4                if color[y] = RED  
 5                   then color[p[z]] ← BLACK                    ▹ Case 1  
 6                        color[y] ← BLACK                       ▹ Case 1  
 7                        color[p[p[z]]] ← RED                   ▹ Case 1  
 8                        z ← p[p[z]]                            ▹ Case 1  
 9                   else if z = right[p[z]]  
10                           then z ← p[z]                       ▹ Case 2  
11                                LEFT-ROTATE(T, z)              ▹ Case 2  
12                           color[p[z]] ← BLACK                 ▹ Case 3  
13                           color[p[p[z]]] ← RED                ▹ Case 3  
14                           RIGHT-ROTATE(T, p[p[z]])            ▹ Case 3  
15           else (same as then clause  
                         with "right" and "left" exchanged)  
16 color[root[T]] ← BLACK  复制代码
下面,我们来分别处理上述3种插入修复状况。

插入修复状况1:

当前结点的父结点是红色且祖父结点的另外一个子结点(叔叔结点)是红色。java

即以下代码所示:
1 while color[p[z]] = RED  
 2     do if p[z] = left[p[p[z]]]  
 3           then y ← right[p[p[z]]]  
 4                if color[y] = RED  复制代码
此时父结点的父结点必定存在,不然插入前就已不是红黑树。
与此同时,又分为父结点是祖父结点的左子仍是右子,对于对称性,咱们只要解开一个方向就能够了。
在此,咱们只考虑父结点为祖父左子的状况。
同时,还能够分为当前结点是其父结点的左子仍是右子,可是处理方式是同样的。咱们将此归为同一类。
对策:将当前结点的父结点和叔叔结点涂黑,祖父结点涂红,把当前结点指向祖父结点,重新的当前结点从新开始算法。即以下代码所示:
5                   then color[p[z]] ← BLACK                    ▹ Case 1  
 6                        color[y] ← BLACK                       ▹ Case 1  
 7                        color[p[p[z]]] ← RED                   ▹ Case 1  
 8                        z ← p[p[z]]                            ▹ Case 1  复制代码

针对状况1,变化前[当前结点为4结点]:node


变化后:

插入修复状况2:

当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的右子程序员

对策:当前结点的父结点作为新的当前结点,以新当前结点为支点左旋。即以下代码所示:
9                   else if z = right[p[z]]
10                           then z ← p[z]                       ▹ Case 2
11                                LEFT-ROTATE(T, z)              ▹ Case 2复制代码
以下图所示, 变化前[当前结点为7结点]:

变化后:

插入修复状况3:

当前结点的父结点是红色,叔叔结点是黑色,当前结点是其父结点的左子面试

解法:父结点变为黑色,祖父结点变为红色,在祖父结点为支点右旋,操做代码为:
12                           color[p[z]] ← BLACK                 ▹ Case 3
13                           color[p[p[z]]] ← RED                ▹ Case 3
14                           RIGHT-ROTATE(T, p[p[z]])            ▹ Case 3复制代码
最后,把根结点涂为黑色,整棵红黑树便从新恢复了平衡。
以下图所示[当前结点为2结点]

变化后:

红黑树的删除

ok,接下来,我们最后来了解,红黑树的删除操做。
"咱们删除的结点的方法与常规二叉搜索树中删除结点的方法是同样的,若是被删除的结点不是有双非空子女,则直接删除这个结点,用它的惟一子结点顶替它的位置,若是它的子结点都是空结点,那就用空结点顶替它的位置,若是它的双子全为非空,咱们就把它的直接后继结点内容复制到它的位置,以后以一样的方式删除它的后继结点,它的后继结点不多是双子非空,所以此传递过程最多只进行一次。”

二叉查找树的删除

继续讲解以前,补充说明下二叉树结点删除的几种状况,待删除的结点按照儿子的个数能够分为三种:
  1. 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
  2. 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
  3. 有两个儿子。这是最麻烦的状况,由于你删除结点以后,还要保证知足搜索二叉树的结构。其实也比较容易,咱们能够选择左儿子中的最大元素或者右儿子中的最小元素放到待删除结点的位置,就能够保证结构的不变。固然,你要记得调整子树,毕竟又出现告终点删除。习惯上你们选择左儿子中的最大元素,其实选择右儿子的最小元素也同样,没有任何差异,只是人们习惯从左向右。这里我们也选择左儿子的最大元素,将它放到待删结点的位置。左儿子的最大元素其实很好找,只要顺着左儿子不断的去搜索右子树就能够了,直到找到一个没有右子树的结点。那就是最大的了。
二叉查找树的删除代码以下所示:
TREE-DELETE(T, z)
 1  if left[z] = NIL or right[z] = NIL
 2      then y ← z
 3      else y ← TREE-SUCCESSOR(z)
 4  if left[y] ≠ NIL
 5      then x ← left[y]
 6      else x ← right[y]
 7  if x ≠ NIL
 8      then p[x] ← p[y]
 9  if p[y] = NIL
10      then root[T] ← x
11      else if y = left[p[y]]
12              then left[p[y]] ← x
13              else right[p[y]] ← x
14  if y ≠ z
15      then key[z] ← key[y]
16           copy y's satellite data into z 17 return y复制代码

红黑树的删除和删除修复

OK,回到红黑树上来,红黑树结点删除的算法实现是:
RB-DELETE(T, z) 单纯删除结点的总操做
1 if left[z] = nil[T] or right[z] = nil[T]  
 2    then y ← z  
 3    else y ← TREE-SUCCESSOR(z)  
 4 if left[y] ≠ nil[T]  
 5    then x ← left[y]  
 6    else x ← right[y]  
 7 p[x] ← p[y]  
 8 if p[y] = nil[T]  
 9    then root[T] ← x  
10    else if y = left[p[y]]  
11            then left[p[y]] ← x  
12            else right[p[y]] ← x  
13 if y ≠ z  
14    then key[z] ← key[y]  
15         copy y's satellite data into z 16 if color[y] = BLACK 17 then RB-DELETE-FIXUP(T, x) 18 return y 复制代码
“在删除结点后,原红黑树的性质可能被改变,若是删除的是红色结点,那么原红黑树的性质依旧保持,此时不用作修正操做,若是删除的结点是黑色结点,原红黑树的性质可能会被改变,咱们要对其作修正操做。那么哪些树的性质会发生变化呢,若是删除结点不是树惟一结点,那么删除结点的那一个支的到各叶结点的黑色结点数会发生变化,此时性质5被破坏。若是被删结点的惟一非空子结点是红色,而被删结点的父结点也是红色,那么性质4被破坏。若是被删结点是根结点,而它的惟一非空子结点是红色,则删除后新根结点将变成红色,违背性质2。”
RB-DELETE-FIXUP(T, x) 恢复与保持红黑性质的工做
1 while x ≠ root[T] and color[x] = BLACK  
 2     do if x = left[p[x]]  
 3           then w ← right[p[x]]  
 4                if color[w] = RED  
 5                   then color[w] ← BLACK                        ▹  Case 1  
 6                        color[p[x]] ← RED                       ▹  Case 1  
 7                        LEFT-ROTATE(T, p[x])                    ▹  Case 1  
 8                        w ← right[p[x]]                         ▹  Case 1  
 9                if color[left[w]] = BLACK and color[right[w]] = BLACK  
10                   then color[w] ← RED                          ▹  Case 2  
11                        x ← p[x]                                ▹  Case 2  
12                   else if color[right[w]] = BLACK  
13                           then color[left[w]] ← BLACK          ▹  Case 3  
14                                color[w] ← RED                  ▹  Case 3  
15                                RIGHT-ROTATE(T, w)              ▹  Case 3  
16                                w ← right[p[x]]                 ▹  Case 3  
17                         color[w] ← color[p[x]]                 ▹  Case 4  
18                         color[p[x]] ← BLACK                    ▹  Case 4  
19                         color[right[w]] ← BLACK                ▹  Case 4  
20                         LEFT-ROTATE(T, p[x])                   ▹  Case 4  
21                         x ← root[T]                            ▹  Case 4  
22        else (same as then clause with "right" and "left" exchanged)  
23 color[x] ← BLACK  复制代码
“上面的修复状况看起来有些复杂,下面咱们用一个分析技巧:咱们从被删结点后来顶替它的那个结点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,咱们不是把红黑树的结点加上除红与黑的另外一种颜色,这里只是一种假设,咱们认为咱们当前指向它,所以空有额外一种黑色,能够认为它的黑色是从它的父结点被删除后继承给它的,它如今能够容纳两种颜色,若是它原来是红色,那么如今是红+黑,若是原来是黑色,那么它如今的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。如今只要恢复其它性质就能够了,作法仍是尽可能向根移动和穷举全部可能性。"--saturnman。
若是是如下状况,恢复比较简单:
  • a)当前结点是红+黑色
解法,直接把当前结点染成黑色,结束此时红黑树性质所有恢复。
  • b)当前结点是黑+黑且是根结点, 解法:什么都不作,结束。
但若是是如下状况呢?:
  • 删除修复状况1:当前结点是黑+黑且兄弟结点为红色(此时父结点和兄弟结点的子结点分为黑)
  • 删除修复状况2:当前结点是黑加黑且兄弟是黑色且兄弟结点的两个子结点全为黑色
  • 删除修复状况3:当前结点颜色是黑+黑,兄弟结点是黑色,兄弟的左子是红色,右子是黑色
  • 删除修复状况4:当前结点颜色是黑-黑色,它的兄弟结点是黑色,可是兄弟结点的右子是红色,兄弟结点左子的颜色任意
此时,咱们须要调用RB-DELETE-FIXUP(T, x),来恢复与保持红黑性质的工做。
下面,我们便来分别处理这4种删除修复状况。

删除修复状况1:

当前结点是黑+黑且兄弟结点为红色(此时父结点和兄弟结点的子结点分为黑)。算法

解法:把父结点染成红色,把兄弟结点染成黑色,以后从新进入算法(咱们只讨论当前结点是其父结点左孩子时的状况)。此变换后原红黑树性质5不变,而把问题转化为兄弟结点为黑色的状况(注:变化前,本来就未违反性质5,只是为了把问题转化为兄弟结点为黑色的状况)。 即以下代码操做:
//调用RB-DELETE-FIXUP(T, x) 的1-8行代码
 1 while x ≠ root[T] and color[x] = BLACK
 2     do if x = left[p[x]]
 3           then w ← right[p[x]]
 4                if color[w] = RED
 5                   then color[w] ← BLACK                        ▹  Case 1
 6                        color[p[x]] ← RED                       ▹  Case 1
 7                        LEFT-ROTATE(T, p[x])                    ▹  Case 1
 8                        w ← right[p[x]]                         ▹  Case 1复制代码
变化前:

变化后:

删除修复状况2: 

当前结点是黑加黑且兄弟是黑色且兄弟结点的两个子结点全为黑色。bash

解法:把当前结点和兄弟结点中抽取一重黑色追加到父结点上,把父结点当成新的当前结点,从新进入算法。(此变换后性质5不变),即调用RB-INSERT-FIXUP(T, z) 的第9-10行代码操做,以下:
//调用RB-DELETE-FIXUP(T, x) 的9-11行代码
9                if color[left[w]] = BLACK and color[right[w]] = BLACK
10                   then color[w] ← RED                          ▹  Case 2
11                        x p[x]                                  ▹  Case 2复制代码
变化前:

变化后:

删除修复状况3:

当前结点颜色是黑+黑,兄弟结点是黑色,兄弟的左子是红色,右子是黑色。spa

解法:把兄弟结点染红,兄弟左子结点染黑,以后再在兄弟结点为支点解右旋,以后从新进入算法。此是把当前的状况转化为状况4,而性质5得以保持,即调用RB-INSERT-FIXUP(T, z) 的第12-16行代码,以下所示:
//调用RB-DELETE-FIXUP(T, x) 的第12-16行代码
12                   else if color[right[w]] = BLACK
13                           then color[left[w]] ← BLACK          ▹  Case 3
14                                color[w] ← RED                  ▹  Case 3
15                                RIGHT-ROTATE(T, w)              ▹  Case 3
16                                w ← right[p[x]]                 ▹  Case 3复制代码
变化前:

变化后:

删除修复状况4:

当前结点颜色是黑-黑色,它的兄弟结点是黑色,可是兄弟结点的右子是红色,兄弟结点左子的颜色任意。设计

解法:把兄弟结点染成当前结点父结点的颜色,把当前结点父结点染成黑色,兄弟结点右子染成黑色,以后以当前结点的父结点为支点进行左旋,此时算法结束,红黑树全部性质调整正确,即调用RB-INSERT-FIXUP(T, z)的第17-21行代码,以下所示:
//调用RB-DELETE-FIXUP(T, x) 的第17-21行代码
17                         color[w] ← color[p[x]]                 ▹  Case 4
18                         color[p[x]] ← BLACK                    ▹  Case 4
19                         color[right[w]] ← BLACK                ▹  Case 4
20                         LEFT-ROTATE(T, p[x])                   ▹  Case 4
21                         x ← root[T]                            ▹  Case 4复制代码
变化前:

变化后:

本文参考

本文参考了算法导论、STL源码剖析、计算机程序设计艺术等资料。

最后

欢迎你们关注个人公种浩【程序员追风】,整理了2019年多家公司java面试题资料100多页pdf文档,文章都会在里面更新,整理的资料也会放在里面。
相关文章
相关标签/搜索