一步一步从二叉查找树学到红黑树

二叉树查找树

又叫二叉排序树。二叉查找树或者是一棵空树,或者是一棵具有如下性质的二叉树:
 对于任何一个结点X
                若它的左子树非空,则左子树上所有结点的值均小于等于X的值;
                若它的右子树非空,则右子树上所有结点的值均大于等于X的值;
 按中序遍历二叉查找树,所得到的中序遍历序列是一个递增(或递减)的有序序列。
 我们来说二叉查找树当然是和查找操作有关系,查找的时间复杂度是和高度H成正比的。也可以这么说,二叉查找树基本操作的时间都是和树的高度H是成正比的。这些基本操作包括查找插入和删除,前驱后继等。遍历不在这里讲,以后会专门写有关遍历的文章,BFS,DFS,无栈非递归等。

查找

我们先来看一下查找:给定一个树的树根指针x和关键字k,返回指向关键字的指针,否则返回NIL。
TREE-SEARCH(x,k)伪代码

《一步一步从二叉查找树学到红黑树》

在递归查找的过程中遇到的结点即构成了一条由树根下降的路径,故TREE-SEARCH的时间复杂度是O(H)。
再来一个非递归版本的ITERATIVE-TREE-SEARCH,这个比递归的要快一些。

《一步一步从二叉查找树学到红黑树》

最大值和最小值

那么如何去找最大值和最小值呢?
最大值肯定是二叉树的最右结点啦,从根结点开始沿着right指针一路走到right指针为空。而最小值肯定在最左结点,从根结点沿着left指针一路走到left指针为空。(此时说的二叉树是按中序遍历是从小到大)

《一步一步从二叉查找树学到红黑树》       《一步一步从二叉查找树学到红黑树》

 时间复杂度都是O(H),因为寻找的过程就是根结点一路下降的过程。

前驱和后继

找到最大值和最小值还不够,有时候我们需要寻找前驱和后继。如何寻找呢?我们这里来讲述一下中序遍历的的前驱和后继寻找。后序遍历和前序遍历的前驱后继的寻找和这是一样的道理。

后继

先说后继,对于一个结点X来说,后继无非是有3种情况:
1  X有右孩子,那么X的后继就是右孩子的最左结点。
2  X无右孩子,而且X是其父母的左孩子,那么X后继就是其父母结点(可能NIL)。
3  X无右孩子,而且X是其父母的右孩子,那么X的后继就是X的某个祖先(这个最低祖先的左孩子也是X的祖先),此时让父母成为新的X,寻找不断找X是其父母的左孩子的结点,如果此时出现一个X结点是其父母左孩子,那个父母就是X的后继。这个意思也就是说把第三种情况变成第二种情况。如果这个时候找不到则得到的是NIL结点。因为最大值是没有后继的,其他的肯定都有。
对于第一种情况很简单

《一步一步从二叉查找树学到红黑树》

对于第二种情况,更简单了,可能父母是NIL,即X结点是根结点。

《一步一步从二叉查找树学到红黑树》

对于第三种情况,找不到的话就是NIL。最大值没有后继。

《一步一步从二叉查找树学到红黑树》          《一步一步从二叉查找树学到红黑树》

伪代码

《一步一步从二叉查找树学到红黑树》

前驱

 再来看一下前驱,前驱和后继是对称的。如果你仔细看,你会发现刚才寻找后继2 、3 情况的过程是X一直向上查找X是否是其父母的左孩子,而此时X的前驱的分类是和他对称的,是一直向上寻找看X是否是其父母的右孩子,那么其父母就是其前驱。
 1  X有左孩子,那么X的前驱就是左孩子的最右结点。
 2  X无左孩子,而且X是其父母的右孩子,那么X前驱就是其父母结点(可能NIL)。
 3  X无左孩子,而且X是其父母的左孩子,那么X的前驱就是X的某个祖先(这个最低祖先的右孩子也是X的祖先),此时让父母成为新的X,寻找不断找X是其父母的右孩子的结点,如果此时出现一个X结点是其父母右孩子,那个父母就是X的前驱。这个意思也就是说把第三种情况变成第二种情况。如果这个时候找不到则得到的是NIL结点。最小值没有前驱。
对应第一种情况

《一步一步从二叉查找树学到红黑树》

对应于第二种情况,父母可能是NIL。

《一步一步从二叉查找树学到红黑树》

对应于第三种情况,找不到是NIL。最小值没有前驱。

《一步一步从二叉查找树学到红黑树》       《一步一步从二叉查找树学到红黑树》

伪代码
TREE-PREDECESSOR(x)
1 if left[x]≠NIL
2    then return TREE-MAXIMUM(left[x])
3 y←p[x]
4 while y≠NIL and x=left[y]
5      do  x←y
6          y←p[y]
7 return y
 时间复杂度都是O(H),就是一条自结点向上的路径罢了。
 综上所述 对于一颗高度为H的二叉查询树来说,静态操作SEARCH,MINIMUN,MAXIMUM,SUCCESSOR,PREDECESSOR等用时都是O(H)。
 对于一颗二叉树,要的不仅仅是静态操作,还需要动态操作。因为必然会有插入和删除操作,而且还要保持二叉排序树的性质。

插入

先来看如何插入结点Z,插入很简单的。先把结点Z的左右孩子left[z] 、right[z]和父母p[z]初始化,然后从根结点开始一直向下寻找Z的父母,找到之后就把孩子Z插入,如果是空树,直接把Z当成根结点。
在伪代码中,X初始化为根结点,Y始终是其父母,最后把Z插入。

《一步一步从二叉查找树学到红黑树》

TREE-INSERT的时间复杂度是O(H)。

删除

插入结点很简单,那么如何删除结点呢?删除结点之后还要保持中序遍历是一个相对位置不变的序列。对于删除一个结点Z,其实是要分类的。
1 如果Z没有孩子,直接删除,仅仅修改其父结点P[Z]的孩子为NIL即可。
2 如果Z仅仅只有一个孩子,那么删除Z之前要把Z的孩子赋值给其父母P[Z]的孩子(左|右)。这样一个拉链直接去删除Z了。
3 如果Z有两个孩子,这样就有点麻烦了。因为删除Z之后还有两个孩子,如果不好好处理会破坏二叉树的性质,我们删除Z一定要保持中序遍历的相对位置序列不变。这样有两种方法,因为只有保持性质即可,两种方法得来的二叉树不一定是一样的,但是都是正确的二叉树。
我们来看一下第三种情况的两种方法:假设删除的结点是P,而且中序遍历的序列是{….CL ,C….  QL,Q,SL,S,P,PR,F}删除P之后的中序遍历序列应该是{….CL ,C….  QL,Q,SL,S,PR,F}。S是P的前驱,PR是P的后继。
《一步一步从二叉查找树学到红黑树》

这就是未删除P之前的二叉树原型。

方法一

第一种方法是,先把P的前驱S的左孩子SL(S肯定无右孩子,左孩子可能是NIL)变成S父母Q的右孩子,删除S。然后把S的值全部给P,这就让P前驱S取代了P的位置,这样序列相对位置没有发生变化。
《一步一步从二叉查找树学到红黑树》

 可是真的是这样吗?这是老严数据结构书上的截图,算导上给的是找后继结点,然后和上边说的一样,就是把刚才的前驱换成后继。
 可是我想说的是这里忽略了一种情况,那就是如果前驱是其左孩子了怎么办?根据之前分析的前驱分类,在这里要删除的结点P肯定有两个孩子,那么对于前驱来说就只有两种情况,如果P前驱是左孩子的最右结点,刚才的分析没有一点问题,可是如果P前驱是其左孩子,也就是说左子树根结点没有右子树。那么按刚才的分析 就是说不通了。前驱S是P的左孩子,你还要把S的左孩子变成S的父母(P)的右孩子吗?那之前P的右孩子岂不是丢了??!!
 所以这个时候你要判断一点就是P前驱是否是P的左孩子,就是在找前驱的时候保留一个指针指向前驱的父母,最后判断是否和P相等,如果相等就把P左孩子的左孩子变成P的左孩子,然后P的左孩子(此时也是前驱)覆盖P。如果不相等,就按刚才说的做。
 算导给的,和这是一样的,只不过是对称找后继,如果后继是其右孩子也是尴尬。也可以和刚才一样,留个指针判断。
 我们要分类判断,如果判断得到P的前驱是其左孩子的话,那么直接将前驱取代P就好了,其他什么都不用做。或者像算导上说的找后继,后继是其右孩子的话就让后继取代他。也就是说如果一个结点的前驱或者后继是其孩子的话,直接让其孩子取代他就好了,很简单的,其他什么都不用做。
 不能想当然的以为前驱一定是左孩子的左右结点,后继一定是右孩子的最左结点。这样就算是前驱不是其左孩子,只要后继是其右孩子,就让右孩子取代他,两个孩子满足一个就可以减少很多的处理,优化算法,而且此时不必留指针再判断了。如何判断呢?如果P的左孩子没有右子树,那前驱就是左孩子。如果他的右孩子没有左子树,那后继就是其右孩子。
伪代码
TREE-DELETE(T,z)
1 if left[z]=NIL or right[z]=NIL
2    then if left[z]=NIL
3            then  p[right[z]]=p[z]
4                  left[p[z]]=right[z]
5         else if right[z]=NIL
6            then p[left[z]]=p[z]
7                 left[p[z]]=left[z]
8 else if  right[left[z]]=NIL
9          then key[z]=key[left[z]]
10              p[left[left[z]]]=z
11              left[z]=left[left[z]]
12 else if left[right[z]]=NIL
13         then p[right[right[z]]]=z
14              right[z]=right[right[z]]
15 else
16      then y=left[z]
17           while right[y]≠NIL
18                 y=right[y]
19           p[left[y]]=p[y]
20           right[p[y]]=left[y]
21           key[z]=key[y]

 这是一种分类的做法,老严给的版本是找后继判断,算导给的是找前驱判断,我这里给他综合了一下,更加的优化。不过算导版本虽然没有分类,但是有各种判断,代码很紧凑。虽然我感觉没我的好,不过还是在这里贴一下。

《一步一步从二叉查找树学到红黑树》

方法二

 接下来我们看第二种方法,这种方法的好处是不像刚才那么有BUG,不过好像大多数书上举的例子都是第一种,汗。难道第一种比第二种的简单?我们看一下吧
 先令P的右孩子PR成为其前驱S的右孩子,然后将P的左孩子C成为P父母F的左孩子,删除P。这样就不用管C是不是其前驱了。C就是其前驱也是把PR给C的右孩子,序列的相对位置是不会发生变化的。

《一步一步从二叉查找树学到红黑树》

来看一下伪代码
TREE-DELETE(T,z)
1 if  left[z]=NIL or right[z]=NIL
2     then if left[z]=NIL
3             then  p[right[z]]=p[z]
4                   left[p[z]]=right[z]
5          else if right[z]=NIL
6                  then p[left[z]]=p[z]
7                       left[p[z]]=left[z]
8  else 
9       y=TREE-PREDECESSOR(z)
10      p[right[z]]=y
11      right[y]=right[z]
12      p[left[z]]=p[z]
13      left[p[z]]=left[z]

代码很简洁,我反倒觉得这个简单,还不麻烦。
这两种方法都很简单的,一时想不起,就拿笔画一颗二叉树,很快就明白了。一定要动手画一下。时间复杂度很明显是O(H)。

上边所说的二叉树的大部分操作的时间复杂度都是O(H),H是树的高度。大家一听高度想当然以为就是lgN,其实非也。一个好的二叉树高度可能是O(lgN),即便是如此,可是经过了很多的插入和删除,高度就会发生变化。就像是一开始只有一个结点的二叉树,最后变成了一个高度为N-1的链。这虽然还是树,可是已经不是我们需要的了,时间复杂度太大了(O(N)).
可是我们怎么才能保证一个树的高度是lg级的呢?随机。一个N个关键字随机的构造的二叉树的高度是lgN。可是经过删除和再次插入的洗礼,高度也不会一直是lgN。所以我们二叉树期望高度是O(lgN),平均时间复杂度是良好的,不过还是不可避免的是会出现最坏情况。这个时候我们就不能仅仅依靠随机了。

平衡二叉树

不过我们不会任由这种最坏情况的发生,我们可以对二叉树做一些平衡的处理,接下来就介绍一下平衡二叉树,这个在最坏情况的时间复杂度都是lg级。
平衡二叉树是为了避免刚才的最坏情况产生的,平衡是指所有的叶子的高度趋于平衡,更广义的是指在平衡二叉树上所有可能查找的均摊复杂度偏低。几乎所有平衡树的操作都基于树的旋转操作,通过旋转操作可以使得树趋于平衡。AVL树,红黑树,伸展树,Treap树等都是平衡二叉树。

AVL树

AVL树仅仅只是平衡二叉树的一种而已,具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵AVL树。若二叉树上结点的平衡因子定义为该结点的左子树的高度减去右子树的高度。那么AVL树的平衡因子就只能是0,-1,1。如果有一个结点的平衡因子的绝对值大于1 ,那么这个二叉树就不是AVL树。如果由于插入和删除结点导致AVL树不平衡,那么就从这个结点开始进行一次或多次的左右旋转和高度的调整,最后使二叉树变得平衡。
老严那本书上讲的平衡二叉树仅仅只是AVL算法的一种平衡二叉树而已,不代表全部的。注意平衡二叉树一种平衡树,而AVL算法只不过是调整树的高度保持平衡的算法而已,不是平衡二叉树的全部。平衡二叉树有很多种,这里我把一些以前的AVL笔记中中的旋转K过来看一下吧。
《一步一步从二叉查找树学到红黑树》                           《一步一步从二叉查找树学到红黑树》

《一步一步从二叉查找树学到红黑树》                         《一步一步从二叉查找树学到红黑树》

红黑树

这里我们主要是说一个特殊的平衡二叉树,那就是红黑树RB-TREE。RB-TREE的好处在于检索很快,主要用于关联式容器。Set map multiset  multimap的底层实现都是RB-TREE,这些关联容器存入的时候都是自动排序,查询的时候都是很快。不过代价就是RB-TREE树的插入和删除带来的麻烦很多,不过还好的是 它可以在O(log n)时间内做查找,插入和删除。
红黑树在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。每一个结点有5个域:color key  left  right  p。如果某个结点没有孩子或没有父结点,则把相应的结点设为NIL,颜色BLACK。
红黑树是每个结点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求(性质):
性质1. 每个结点只能是红色或黑色。 性质2. 根结点是黑色。 性质3. 所有叶子结点都是黑色(叶子是NIL结点)。 性质4. 每个红色结点的两个孩子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点) 性质5. 从任一结点到其每个叶子的所有简单路径都包含相同数目的黑色结点。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

《一步一步从二叉查找树学到红黑树》

 在很多树数据结构的表示中,一个结点有可能只有一个子结点,而叶子结点包含数据。这种表示红黑树是可能的,但是这会改变一些属性并使算法复杂。为此,我们使用 NIL来表示叶子结点。如上图所示,它不包含数据而只充当树在此结束的指示。这些结点在绘图中经常被省略,导致了这些树好像同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有结点都有两个子结点,尽管其中的一个或两个可能是空叶子NIL。有点时候为了节省内存把所有的NIL连在一起当成一个NIL[T]哨兵,代表所有的叶子NIL和父母结点是NIL的结点。
 结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
 要知道为什么这些特性确保了这个结果,注意到性质4导致了路径不能有两个相连的红色结点就足够了。最短的可能路径都是黑色结点,最长的可能路径有交替的红色和黑色结点。还有注意的是叶子节点都是NIL,它不包含数据而只充当树在此结束的指示。所以根据属性5所有最长的路径都有相同数目的黑色结点,这就表明了没有路径能多于任何其他路径的两倍长。
 我们定义黑高度BH(X)为从结点X出发(不包括X)到叶子结点的任意一条路径上,黑色结点的个数。这样我们就会得到一个结论:一颗有N个结点(不包括NIL叶子)的红黑二叉树的高度至多是2lg(N+1)。
 简单证明一下:由于以X为根的RB-TREE至少包含2^BH(X)-1个结点。因为对于高度为0的RB树,结点数目是0,对于一个结点高度大于0的RB树,左右孩子的颜色可能是红,也可能是黑,所以孩子的黑高度BH有可能和父母一样,也有可能比父母少1。所以孩子的结点数目加上父母结点X就和X为根的RB树的结点数目是一样的,也就是说2^(BH(X)-1)-1+2^(BH(X)-1)-1+1=2^BH(X)-1,所以假设成功。此时呢,假设N个结点的RB树高度是H,则黑高度至少是H/2,所以呢,2^BH-1=2^(H/2)-1<=N,H<=2lg(N+1),得证。
 所以对于红黑树的所有动态和静态的操作都可以在O(lgN)时间内实现。

旋转

可是经过了删除和插入,红黑树的性质会发生变化,所以需要一些旋转操作和结点颜色的改变来维持性质。这里我们先来看一下旋转的知识。旋转分左旋和右旋。
《一步一步从二叉查找树学到红黑树》

 当在某个结点x上做左旋操作时,我们假设它的右孩子y不是NIL,x可以为RB树内任意右孩子而不是的结点。旋以x到y之间的链为“支轴”逆时针旋转,它使y成为该子树新的根,而y的左孩子b则成为x的右孩子。而右旋和左旋是对称的。Y的右孩子X也不是NIL,这时候以y到x之间的链为“支轴”逆顺时针旋转,使x成为新的子树的根,x的右孩子变成y的左孩子。因为左右对称,这里我们只给出左旋的伪代码。假设X的右孩子不等于NIL,而且根的父母结点是NIL。

《一步一步从二叉查找树学到红黑树》

 时间复杂度都是O(1),旋转的时候只有指针在变化,而其他的所有域都不会改变。至于RB树颜色的变化是和旋转无关的。还要明确一点的就是旋转不改变中序遍历的相对顺序

插入

现在就先说插入吧,因为插入毕竟比删除简单一些。在一个有N个RB树中插入一个结点可以在O(lgN)时间内完成。
插入一个结点肯定是要赋予颜色的,我们是给他红色,还是给他黑色呢?
5大性质:
性质1. 每个结点只能是红色或黑色。
性质2. 根结点是黑色。 性质3. 所有叶子结点都是黑色(叶子是NIL结点)。 性质4. 每个红色结点的两个孩子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)
性质5. 从任一结点到其每个叶子的所有简单路径都包含相同数目的黑色结点。
根据5大性质我们知道,如果把插入的结点赋予黑色,则会违反性质5,导致黑高度发生变化,这个是很麻烦的。待会删除的时候你就会明白。而如果赋予红色,则只会违反性质2和4,如果是性质2,说明插入的是根结点,直接把根结点变成黑色就好。如果违反性质4,说明插入结点的父节点是红色的,此时可以通过旋转和结点颜色的改变搞定,这个不是很麻烦。我们只讨论违反性质的情况,不违反的就不说了。我们来看一下。
RB-TREE的插入和之前的二叉查找树的插入代码有些不同,所有的NIL结点换成哨兵NIL[T],插入的结点初始化left和right都是哨兵NIL[T],颜色直接赋予红色。然后专门写一个调整算法RB-INSERT-FIXUP。这里我们只看调整算法,之前的插入我们已经讲过了,不明白看前面章节。
我们假设父节点是P,叔叔结点是U,祖父结点是G,插入结点是N。同时假设Z的父节点P是祖父G的左孩子,这和是右孩子是对称的,我们只讨论P是G左孩子的情况。
如何调整呢?我们要寻找一些辅助,父节点P肯定是要参与的,因为要看他是不是红色,红色的话就调整,不是红色的话直接给根结点变黑色(不判断都行),不管其他的。而且可以肯定是祖父节点G肯定是存在的,而且是黑色(NIL或者其他,因为插入之前肯定满足RB树的性质)。如果父结点P是红色,那么我们如何调整?因为除了根结点是黑色之外,其他的都不晓得,我们也不能随意的调整。

情况一

 我们不能贸然调整把插入结点N变成黑色,不过如果父结点的兄弟结点,也是插入结点N的叔叔结点U,他如果是红色的话。

《一步一步从二叉查找树学到红黑树》

 此时我们可以把U和P都变成黑色,但是此时以祖父的父节点G为根的子树的黑高度变化了,也即是+1了。这就违反了性质5,唉,千万不敢违反性质5,超级麻烦。所以我们做一个变通,可以把祖父结点G变成红色,此时的违反性质的可能性就落到了G身上,这时就把G当成插入结点N,继续进入调整算法开始调整。
 也就是说我们可以把这个插入结点N和父节点P的连续红色的违反情况向上移动,就是把违反性质的结点向上调整,这样直到最后变成根结点是红色的,根结点父节点是NIL[T]黑色,此时直接把根结点变成黑色即可,很nice。

《一步一步从二叉查找树学到红黑树》

 此时的G就变成了新的插入结点了,继续调整,最多到根结点。

情况一:如果父节点P和叔父节点U二者都是红色,则我们可以将它们两个重绘为黑色并重绘祖父节点G为红色(用来保持性质5)。

 可是如果叔叔结点U是黑色呢?这就不能按刚才说的调整了,我们要换一个策略了。此时我们要分情况,插入结点N是父节点P的左孩子还是右孩子,这是不一样的。

情况三

如果N是P的左孩子
《一步一步从二叉查找树学到红黑树》

 此时我们直接把父节点P变成黑色,然后以G--P为轴顺时针旋转(右旋),P变成了根,但是G变成了P的右孩子,这个时候两边的黑高度就发生了变化,右边的+1了,此时我们可以把G变成红色,这样两边就均衡了啊。而且
此时算法直接就结束了,因为性质都保持住了。

《一步一步从二叉查找树学到红黑树》

情况二

可是如果N是P的右孩子呢?

《一步一步从二叉查找树学到红黑树》

 这样看起来很不好搞啊,旋转变色,都达不到要求,都是无用的。我们可以把这个N右孩子变成刚才说的左孩子吗?看起来是不可以。不过我们可以发现的是父母和孩子结点的变换是可以通过旋转来做的,也就是说通过一次旋转,孩子可以变成父母,父母也可以变成孩子,而新的孩子和之前的孩子的左右是相反的。而此时N和P都是红色,我们通过一次的左旋,可以把P变成N的左孩子,也就是相当于P变成插入结点是红色,N变成父节点是红色,叔叔结点U是黑色,则和刚才说的是一样的。

《一步一步从二叉查找树学到红黑树》

 也即是变成了刚才那种情况。所以我们把这种情况称为第二种,刚才说的是第三种。因为第二种到第三种是直接过度的,不是互斥的情况。虽然我们忘了一种情况,那就是叔叔不存在,但是这并不影响我们刚才分析的结果。
情况二:如果父节点P是红色而叔父节点U是黑色或不存在,并且插入节点N是其父节点P的右孩子。我们以P-N为轴进行一次左旋调换插入节点N和其父节点P的角色(不是颜色)

《一步一步从二叉查找树学到红黑树》

情况三::如果父节点P是红色而叔父节点U是黑色或不存在,并且插入节点N是其父节点P的左孩子。我们可以把P重绘为黑色,并重绘G为红色。然后以P-G为轴以进行一次左旋调换节点P和节点G的角色。
 情况一要进入继续调整,最坏的情况是向上调整到根。情况二直接进入情况三。情况三调整完了之后调整算法就结束了。
 上伪代码,这个伪代码和刚才分析的是一样的,如此同时还是只有半边代码,就是只有父节点P是祖父结点G的左孩子的情况。不过在伪代码里,插入结点是z,y是其叔叔。
INSERT伪代码

《一步一步从二叉查找树学到红黑树》

RB-INSERT-FIXUP伪代码

《一步一步从二叉查找树学到红黑树》

给一个例子看看,很好的例子,囊括了三种情况。
 这是情况一的情况

《一步一步从二叉查找树学到红黑树》

情况一调整结果变成了情况二了。

《一步一步从二叉查找树学到红黑树》

情况二调整结果变成了情况三了。

《一步一步从二叉查找树学到红黑树》

情况三调整完了就结束了。

《一步一步从二叉查找树学到红黑树》

 很明显我们的RB-INSERT的1-16行的时间代价是(lgN),而对于RB-INSERT-FIXUP中,只有情况一才会发生循环,即便是循环到根,也不过用时O(lgN),所以总的时间花费就是O(lgN)。而且这个RB-INSERT-FIXUP过程的旋转不超过2次,也就是情况二和情况三各旋转一次O(1),旋转过后算法就结束了。

删除

接下来我们来看看如何删除RB-TREE的结点。
 对于一个有N个结点的RB-TREE来说,删除一个结点要花费的时间就是O(lgN)。与插入相比,只不过是更加的复杂罢了。我们来分析一下。删除一个二叉树的结点我们说过了,这里不再说,之说删除结点之后如何保持红黑树的5大性质。
5大性质:
 性质1. 每个结点只能是红色或黑色。  性质2. 根结点是黑色。  性质3. 所有叶子结点都是黑色(叶子是NIL结点)。  性质4. 每个红色结点的两个孩子结点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色结点)  性质5. 从任一结点到其每个叶子的所有简单路径都包含相同数目的黑色结点。
 如果删除的结点的颜色是一个红色,我们压根就不需要调整,因为这并不违反任何一个性质。因为各结点黑高度没有变化,不存在相邻的红色结点,删除的也不是根结点。如果树的结点只有一个,那么删除了也不需要调整。如果树的结点不是一个而且删除的结点是黑色的,那么就有可能违反性质4,因为可能有两个相邻的红色结点。删除了根结点,造成新的根结点是红色可能违反了性质2。而且删除黑色的结点会违反性质5,导致黑高度不一致。
 我们如何调整呢?假设删除了结点Y,肯定有占用他位置的结点是X,X可能是他的前驱或后继,X也可能是Y的孩子,或者是哨兵NIL[T]结点。如果X结点本身是红色,也就是说有可能违反性质4和5,那我们直接赋予它黑色就解决了。如果红色结点X变成了根违反性质1,也直接赋予黑色就OK。现在的问题是如果X结点是黑色而且不是根结点,违反了性质5,黑高问题就不好解决了。这里我们主要说如何解决这一个情况,前两个情况很easy,不需细说了。
 之前说过如果违反了性质5,会很麻烦,这会你就知道了。违反了性质5,我就很拙计了。当然这也不是解决不了的。那如何保持性质5呢?我们总不能期望他不违反性质5吧。这里我们用一个技巧,就是假设删除的结点Y把它的黑色性质X结点了,也就是说X结点多了一重黑色,就是黑+黑,这样的话黑高度就一致了,性质5就满足了。
 当然这只是假设,我们假想的,不过这有助于我们解决问题。这个黑+黑结点是X结点,他的颜色属性还是黑色,只不过用一个指针指向他,他便多了一层黑色。不断调整的过程,会出现新的X结点,这时候指针指向的结点不一定会是原来的X了,不过还是会多一层黑色。而且在不断旋转的过程会出现旋转前后两边子树黑高发生变化的情况,此时如果N这边的子树黑高+1了,那N头上的多于黑色就没有了,舍弃。这就是我们的最终目的 这也就是调整的麻烦之处,接下来我们看看他如何麻烦。当然这还是要分类的。这里我们只考虑结点X是其父节点左孩子的情况,对称的情况不再讲。
 我们假设占用被删除结点的位置的结点是N,N的兄弟结点是S,N的父亲结点是P,S的左孩子是SL,S是右孩子是SR。要明确一点的是S不是NIL[T],因为如果是NIL[T],那两边的黑高现在就不一样,即便是没删除之前,RB-TREE也不会出现这种情况的。

情况一

如果兄弟结点S是红色,那么S的父亲结点P和S的孩子SL和SR都必然是黑色,此时N也是黑色的。

《一步一步从二叉查找树学到红黑树》

 此时如何调整?指针指向的是N,N是黑+黑。此时父亲结点P变红色,S变黑色,然后以P-S为轴左旋。此时指针指向的还是N,看起来好象是没什么变化,其实是变了,父亲结点P变成了红色,兄弟结点变成SL还是黑色。不要着急,这只是第一步而已嘛。

《一步一步从二叉查找树学到红黑树》

情况一:如果兄弟结点S是红色的(隐含的意思P,SL,SR都是黑色),那么我们就交换父节点P和兄弟结点S的颜色,即P变红色,S变黑色,然后以P-S为轴左旋。

如果兄弟结点是红色的话,只可能有这一种情况,因为父节点和孩子结点的颜色都是定好的黑色。接下来看兄弟结点是黑色的情况,这样他的孩子结点和父亲结点的颜色可就不唯一了,这可以分好几种情况的。

情况二

如果N的兄弟结点S是黑色的,而且两个孩子结点都是黑色的。N还是被指针指向,黑+黑。(P的颜色不晓得
《一步一步从二叉查找树学到红黑树》              
《一步一步从二叉查找树学到红黑树》

 如何调整?此时我们并不知道P的颜色,我们也不需要去判断。我们假设N和S都脱掉一层的黑色(两边路径上的黑色结点数还是相同),那N变成了正儿八经的黑色了,指针也不指向他了,而S脱掉黑色就只能变成了红色。而此时经过P的路径在这边少了一个黑色,黑高-1,所以我们要用指针指向他,给他多一层的黑色,这样经过P的路径在这边的黑色就不变了,这样多好。

《一步一步从二叉查找树学到红黑树》            
《一步一步从二叉查找树学到红黑树》

 此时指针指向的是P(新的N)多了一层黑色,黑+黑或者是红+黑。有变化吗?当然了,看起来好像回到了情况一(兄弟S是红色),其实不是。因为新的N是P,这样指针指向的结点不断的向上移动,至多变成了根结点,那敢情好,直接赋予黑色,OK搞定。
情况二:如果兄弟结点S是黑色的,而且S的两个孩子也都是黑色的。直接把N变成黑色,S变成红色,用指针指向P,P多了一层黑色。

情况三

如果N的兄弟结点S是黑色,不过S的左孩子SL是红色的,右孩子SR是黑色的。
《一步一步从二叉查找树学到红黑树》

 那么如何调整?此时把SL和S的颜色交换,即SL变成黑色,S变成红色,然后以S-SL为轴右旋。此时N的兄弟变成了SL,而SL的右孩子变成了红色。

《一步一步从二叉查找树学到红黑树》

情况三:如果N的兄弟结点S是黑色,而且S的左孩子SL是红色的,右孩子SR是黑色的。此时把SL变成黑色,S变成红色,然后以S-SL为轴右旋。

情况四

此时N的兄弟S的右孩子就变成了红色,而左孩子不晓得。这种情况就是我们要说的情况四,情况三直接过渡到情况四,相互不排斥,其实是在一个分类里面。

《一步一步从二叉查找树学到红黑树》

 这样的话就是直接交换P和S的颜色,P变成黑色,而S变成P的颜色(不定),S的右孩子SR也变成黑色。然后以P-S为轴左旋。此时只有S的颜色不定,可是此时S的孩子都是黑色,S是什么色都无所谓了。此时算法就结束了,调整完毕。

《一步一步从二叉查找树学到红黑树》

算法为什么就结束了呢?因为调整的前后左边的子树的黑高+1了,所以此时N的多一层黑色就没用了,要舍弃。就是说我们解决了黑高的问题,性质5保持住了。
情况四:如果N的兄弟结点S是黑色,而且S的右孩子SR是红色的。那么交换P和S的结点颜色,即S变成P的颜色(不定),P变成黑色,SR变成黑色,然后以P-S为轴左旋 OK,大功告成一半,接下来上伪代码。伪代码中Y是要删除的结点,X是替代Y的结点,W是X的兄弟。调整算法是RB-DELETE-FIXUP。
RB-DELETE(T,x)
........
if color[y]=black
   then RB-DELETE-FIXUP(T,x)

《一步一步从二叉查找树学到红黑树》

 RB-DELETE-FIXUP的时间复杂度是O(lgN),因为只有情况二需要循环,循环次数最多是O(lgN)。情况 1 、3、 4最多需要旋转3次和一些颜色的改变而已。所以RB-DELETE-FIXUP过程要花费O(lgN)的时间,至多需要3次旋转。总的来说红黑树是很不错的,很快,所以很多容器的底层都用RB树。  OK,这样红黑树就差不多说完了,以后有新的理解还会继续更新。 转载请注明出处:http://blog.csdn.net/liangbopirates/article/details/9844819
































    原文作者:二叉查找树
    原文地址: https://blog.csdn.net/liangbopirates/article/details/9844819
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞