《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除

前言:

     这本书学到了最后一章终于出现了红黑树,它不愧为最难的几个数据结构之一,从看书到实现整个红黑树一共用时2天,第一天看书加上实现自顶向下的插入算法大概用了6个小时。

July 的博客里,还有各个知名博主博客里的红黑树基本是使用自底向上的方式来实现删除的,《数据结构与算法分析》这本书上建议使用自顶向下删除,但是对于如何删除,说的特别含糊,基本上不可以参考,于是在网络上寻找是否有详细的讲解,最终,找到一篇英文文献,比较详细的介绍了如何实现《数据结构与算法分析》12.2节中的自顶向下删除的方法。

文献连接:http://www2.ee.ntu.edu.tw/~yen/courses/ds-03/Red-Black-Trees-top-down-deletion.pdf

此文中讲述了各个情况如何旋转红黑树,但是并没有一个流程图,也没有将清楚如何删除,不过自己花了大约10个小时,把整个思路理清楚了,然后完成了自顶向下删除的流程图与伪代码,最终实现了自顶向下的删除过程。

我的github:

我实现的代码全部贴在我的github中,欢迎大家去参观。

https://github.com/YinWenAtBIT

介绍:

红黑树:

一、简介:

红黑树(Red Black Tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。

它是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J.Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。

红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。

它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。

二、红黑树的性质:

性质1. 节点是红色或黑色。

性质2. 根节点是黑色。

性质3 每个叶节点(NIL节点,空节点)是黑色的。

性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

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

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第1张图片

如上是一颗红黑树,在实现红黑树时,我们有一个小技巧,能够巧妙的简化自顶向下的插入与删除操作。

三、平衡树旋转:

平衡树的旋转分成单旋转和双旋转,单旋转分成左旋转和右旋转。

双旋转是由两个单旋转构成的。

旋转的详细内容不是这里要叙述的,如果需要更加详细的介绍可以参考其他博客。

红黑树的旋转同样是使用以上两种旋转来达到其性质要求,主要是保证性质4和5在插入和删除节点时能继续成立。

四、实现小技巧:

在实现红黑树时,我们建立一个NullNode来代表NULL,如果一个节点的孩子指向了NullNode,那么就代表没有孩子。NullNode的颜色设置为黑色,在判断孩子节点颜色的时候,省去了对NULL的特殊处理。NULL的元素值设置为Infinity。

除了NullNode之外,根节点T不是真正的根节点,T中元素设置为NegInfinity,它真正的根在它的右子树上,即T->right才是真正的根,T->left指向NullNode。这样做的好处最开始我一点都不明白,直到实现自顶向下的删除时,才发现这样设计的妙处。

红黑树的操作:

基础操作

一、打印:

打印红黑树就需要从真正的根节点开始了,需要一个驱动程序来驱动真正的打印过程:

inline void PrintTree(RedBlackTree T)
{DoPrint(T->right, 0);}

/*打印红黑树*/
void DoPrint(RedBlackTree T, int depth)
{
	if(T != NullNode)
	{
		DoPrint(T->left, depth +1);
		for(int i =0; iElememt, T->color == Red? "Red":"Black");

		DoPrint(T->right, depth+1);
	}
}


在这里是打印到终端里,打印出红色和黑色,紧跟着权值。下图是一张例子图,插入的点与《数据结构与算法分析》给出的相同,图的上方代表左孩子,下方代表右孩子。

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第2张图片

二、搜索节点:

搜索节点过程中,如果遇到了要找的节点,就返回该节点,如果没有找到,就返回最后停留的节点,而不返回NullNode,这样做的好处是在删除节点时,方便找到子树的最小或者最大节点。

RedBlackTree find(ElementType item, RedBlackTree T)
{
	RedBlackTree Parent;
	while(T != NullNode && T->Elememt != item)
	{
		Parent = T;
		if(item Elememt)
			T =T->left;
		else
			T =T->right;
	}
	if(T == NullNode)
		return Parent;
	else
		return T;
}

插入节点:

插入节点是第一个麻烦的地方,在这里只有把插入的节点设置为红色,才不会影响到树的性质5,但是如果插入节点的父亲节点是红色,那么就麻烦了,成以下几个方式讨论:

情况1:插入的是根结点。

原树是空树,此情况只会违反性质2。

  对策:直接把此结点涂为黑色。

情况2:插入的结点的父结点是黑色。

此不会违反性质2和性质4,红黑树没有被破坏。

  对策:什么也不做。


麻烦的情况:

以下两种情况只考虑插入的父亲节点P是祖父节点G的左子树,另一种镜像的情况可同理推出。

这里设N为插入的节点,即考虑的当前节点,P为N的父亲节点,U为N的兄弟节点,即此时P的右子树。G为P的父亲节点,也就是N的祖父。


情况3:N为红,P为红,U为黑,P为G的左孩子,N为P的左孩子

操作:如图P、G变色,P、G变换即G右旋(镜像情况左旋),结束。

解析:要知道经过P、G变换(旋转),变换后P的位置就是最初G的位置,所以红P变为黑,而黑G变为红都是为了不违反性质5,而维持到达叶节点所包含的黑节点的数目不变!

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第3张图片


情况4:N为红,P为红,U为黑,P为G的左孩子,N为P的右孩子(镜像:P为G的右孩子,N为P的左孩子;反正两方向相反)。

操作:需要进行两次变换(旋转),图中只显示了一次变换-----首先P、N变换,颜色不变;然后就变成了情形3的情况,按照情况3操作,即结束。

解析:由于P、N都为红,经变换,不违反性质5;然后就变成3的情形,此时G与G现在的左孩子变色,并变换,结束。

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第4张图片


最麻烦的情况:

以上两种麻烦的情况只需要完成变换就可以了,这个第五种情况需要递归的进行,这也是自底向上插入时,向上走的目的。


情况5:N为红,P为红,(祖节点一定存在,且为黑,下边同理)U也为红,这里不论P是G的左孩子,还是右孩子;不论N是P的左孩子,还是右孩子。

操作:如图把P、U改为黑色,G改为红色,未结束。然后将N指向G,P指向G的父亲节点,U指向N的兄弟,G也顺势向上移动,然后重新开始执行插入算法。

解析:N、P都为红,违反性质4;若把P改为黑,符合性质4,显然左边少了一个黑节点,违反性质5;所以我们把G,U都改为相反色,这样一来通过G的路径的黑节点数目没变,即符合4、5。此时G变红了,若G的父节点又是红的就有违反了4。所以如果需要把G当做插入节点,重新判断符合以上5种情况的哪一种,然后再处理。

所以经过上边操作后未结束,把G看做一个插入的红节点继续向上检索----属于哪种情况,按那种情况操作~遇上情况2就结束,或者达到根结点(此时根结点为红色,根据红黑树性质,改成黑色,完成插入)。



《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第5张图片

自底向上插入的缺点:

以上5种情况就概括了所有的插入时遇到的情况。需要自底向上处理的情况也就是情况5了。要实现情况5,那么就需要在红黑树结构体里加上父亲指针,或者使用栈的方式来解决递归向上。我参看july的博客,发现基本上都是增加一个父亲节点来解决这个问题。

1. 这样就就使得结构体变大了,这样的情况在自底向上实现AVL树时也出现了(我先自底向上实现了AVL树,觉得太繁琐,又自顶向下实现了一遍)。

2. 加上父亲节点之后,编程变得非常的麻烦,特别是旋转操作时,需要非常小心。(在编写自底向上的AVL树时就吃过亏)


自顶向下插入的改进:

使用自顶向下,改进的就是第5种情况,没有了第5种情况,3和4旋转之后,新的父亲节点都是黑色,不会遇上违反性质4的情况,即旋转之后红黑树就完成了插入。

1. 无需添加父亲指针

2. 编码简单


自顶向下的实现方法:

现在既然已经知道,要避免递归往上判断,就需要避免情况5出现,要避免情况5出现,就只需要一个办法:

让兄弟节点U永远是黑色。


插入步骤:

步骤1:. 从根节点往下,记录父亲节点P,祖父节点GP,祖祖父节点GGP,当前节点X。


步骤2.:如果X有两个红色的孩子,那么就使两个孩子变成黑色,X变成红色。这个过程可能使得X与P都是红色。如果不满足这个条件,就跳到步骤4。

 

步骤3.:如果出现了X与P都是红色,那么此时X的兄弟节点U必定是黑色,因为从上往下的过程,我们已经确定了U肯定是黑色的。那么这就回到了上面所描述的插入中的情况3/4。直接使用单旋转或者双旋转就可以解决。解决之后,让X指向旋转之后的根节点,此时X为黑色,两个孩子为红色,原本的X是指向这两个红色孩子中的其中一个的,我们在这里回退,目的是让GP,GGP随着X的下降,回到正常的值(此时不判定两个孩子是否都为红色,刚刚做的事情就是让两个孩子变成红色,根节点变成黑色,)。


步骤4: 完成过程2(3)之后,继续往下前进,重复过程2,4,直到到达key的节点,或者达到NULL,此时X为NULL。


步骤5:如果到达了key,那么key已经存在,不能再插入,直接返回即可。如果到达NULL,那么X指向插入新的节点,并且设X为红色,并且判断此时的P是否是红色,如果是红色,那么兄弟U必然是黑色,那么再进行一次步骤3,就完成了插入。


实现过程中需要保存GGP节点的原因是,G也会参与到旋转中,那么旋转之后,GGP需要指向新的旋转之后的根。

自顶向下插入编码:

循环往下前进,由于T中不是真正的根,且T中保存最小的值,所以X可以自动指向根,并使得P指向合适的值。在这里,GGP, GP, P, X, BRO都是全局变量,所以我感觉红黑树更合适使用C++的类来实现,这样可以把这些元素保存为类的成员,避免了多线程处理的不安全问题。

RedBlackTree insert(ElementType item, RedBlackTree T)
{
	X = P =GP = T;
	NullNode->Elememt = item;
	while(X->Elememt != item)
	{
		GGP = GP; GP = P; P =X;
		if(itemElememt)
			X = X->left;
		else
			X = X->right;
		if(X->left->color == Red && X->right->color == Red)
			HandleReorient(item, T);
	}
	if(X != NullNode)
		return NullNode;

	X = (RedBlackTree)malloc(sizeof(struct RedBlackNode));
	X->Elememt = item;
	X->left = X->right = NullNode;
	
	if(itemElememt)
		P->left = X;
	else
		P->right = X;

	HandleReorient(item, T);
	return T;
}

其中HandleReorient就是变换颜色,将当前X设置为红色,孩子设置为黑色的程序,所以插入节点时,由于新的节点指向NullNode,并且NullNode的颜色时黑色的,所以可以直接调用这个例程。这就是设置NullNode代替NULL的好处。

下面是static void HandleReorient(ElementType item, RedBlackTree T)函数的实现,其中Rotate函数实现情况3,4的旋转。

static void HandleReorient(ElementType item, RedBlackTree T)
{
	X->color = Red;
	X->left->color = Black;
	X->right->color = Black;

	if(P->color == Red)
	{
		GP->color = Red;
		if((item< P->Elememt) != (item < GP->Elememt))
			//P = Rotate(item, GP);
			Rotate(item, GP);

		X = Rotate(item, GGP);
		X->color =Black;

	}
	T->right->color = Black;
}

Rotate函数实现:

/*需要进行旋转的时候进行旋转,旋转之前应先进行判定*/
static Position Rotate(ElementType item, RedBlackTree Parent)
{
	if(item < Parent->Elememt)
		return Parent->left = itemleft->Elememt? 
			rightSingleRotate(Parent->left):
			leftSingleRotate(Parent->left);
	else
		return Parent->right = item right->Elememt?
			rightSingleRotate(Parent->right):
			leftSingleRotate(Parent->right);
}
到这里自顶向下的插入过程就完成了。

删除节点:
首先贴出自底向上删除的学习博客:http://blog.csdn.net/v_JULY_v/article/details/6105630

这篇博客是详细介绍了如何自底向上的插入与删除,插入的过程与我上面介绍的相似,删除过程非常的复杂,我在这里只基本介绍一下,便于进行对照。

自底向上删除:

算法导论上给出的算法先与普通搜索树相同,先找到该节点,再根据是否是叶子节点决定下一步:

  1. 没有儿子,即为叶结点。直接把父结点的对应儿子指针设为NULL,删除儿子结点就OK了。
  2. 只有一个儿子。那么把父结点的相应儿子指针指向儿子的独生子,删除儿子结点也OK了。
  3. 有两个儿子。这是最麻烦的情况,因为你删除节点之后,还要保证满足搜索二叉树的结构。其实也比较容易,我们可以选择左儿子中的最大元素或者右儿子中的最小元素放到待删除节点的位置,就可以保证结构的不变。当然,你要记得调整子树,毕竟又出现了节点删除。习惯上大家选择左儿子中的最大元素,其实选择右儿子的最小元素也一样,没有任何差别,只是人们习惯从左向右。这里咱们也选择左儿子的最大元素,将它放到待删结点的位置。左儿子的最大元素其实很好找,只要顺着左儿子不断的去搜索右子树就可以了,直到找到一个没有右子树的结点。那就是最大的了。
算法伪代码:

  1. TREE-DELETE(T, z)  
  2.  1  if left[z] = NIL or right[z] = NIL  
  3.  2      then y ← z  
  4.  3      else y ← TREE-SUCCESSOR(z)  
  5.  4  if left[y] ≠ NIL  
  6.  5      then x ← left[y]  
  7.  6      else x ← right[y]  
  8.  7  if x ≠ NIL  
  9.  8      then p[x] ← p[y]  
  10.  9  if p[y] = NIL  
  11. 10      then root[T] ← x  
  12. 11      else if y = left[p[y]]  
  13. 12              then left[p[y]] ← x  
  14. 13              else right[p[y]] ← x  
  15. 14  if y ≠ z  
  16. 15      then key[z] ← key[y]  
  17. 16           copy y's satellite data into z  
  18. 17  return y 

对于红黑树的删除:

  1.  1 if left[z] = nil[T] or right[z] = nil[T]    
  2.  2    then y ← z    
  3.  3    else y ← TREE-SUCCESSOR(z)    
  4.  4 if left[y] ≠ nil[T]    
  5.  5    then x ← left[y]    
  6.  6    else x ← right[y]    
  7.  7 p[x] ← p[y]    
  8.  8 if p[y] = nil[T]    
  9.  9    then root[T] ← x    
  10. 10    else if y = left[p[y]]    
  11. 11            then left[p[y]] ← x    
  12. 12            else right[p[y]] ← x    
  13. 13 if y ≠ z    
  14. 14    then key[z] ← key[y]    
  15. 15         copy y's satellite data into z    
  16. 16 if color[y] = BLACK    
  17. 17    then RB-DELETE-FIXUP(T, x)    
  18. 18 return y 

根据算法导论给出的算法,我们可以先知道,如果被删除的节点是红色的,那么一切好办,如果是黑色的,那么麻烦大了,因为此时已经违背了性质5,并且有可能违背了性质4,所以需要根据各种情况来讨论接下来的处理。

RB-DELETE-FIXUP(T, x) 伪代码:

  1. while x ≠ root[T] and color[x] = BLACK    
  2.  2     do if x = left[p[x]]    
  3.  3           then w ← right[p[x]]    
  4.  4                if color[w] = RED    
  5.  5                   then color[w] ← BLACK                        ▹  Case 1    
  6.  6                        color[p[x]] ← RED                       ▹  Case 1    
  7.  7                        LEFT-ROTATE(T, p[x])                    ▹  Case 1    
  8.  8                        w ← right[p[x]]                         ▹  Case 1    
  9.  9                if color[left[w]] = BLACK and color[right[w]] = BLACK    
  10. 10                   then color[w] ← RED                          ▹  Case 2    
  11. 11                        x ← p[x]                                ▹  Case 2    
  12. 12                   else if color[right[w]] = BLACK    
  13. 13                           then color[left[w]] ← BLACK          ▹  Case 3    
  14. 14                                color[w] ← RED                  ▹  Case 3    
  15. 15                                RIGHT-ROTATE(T, w)              ▹  Case 3    
  16. 16                                w ← right[p[x]]                 ▹  Case 3    
  17. 17                         color[w] ← color[p[x]]                 ▹  Case 4    
  18. 18                         color[p[x]] ← BLACK                    ▹  Case 4    
  19. 19                         color[right[w]] ← BLACK                ▹  Case 4    
  20. 20                         LEFT-ROTATE(T, p[x])                   ▹  Case 4    
  21. 21                         x ← root[T]                            ▹  Case 4    
  22. 22        else (same as then clause with "right" and "left" exchanged)    
  23. 23 color[x] ← BLACK    

一共分成了4种情况,并且此时还是未考虑镜像对称的伪代码,十分复杂,我在此简单介绍一下,不贴出各种情况下的处理图片了,需要处理图片可以参考我上面贴出的JULY的博客。

引人的新概念:

需要一提的是,JULY在这里引入了一个新的概念,便于大家理解其中1-4情况中处理的目的,以及为什么这样处理不违背性质。

“上面的修复情况看起来有些复杂,下面我们用一个分析技巧:我们从被删节点后来顶替它的那个节点开始调整,并认为它有额外的一重黑色。这里额外一重黑色是什么意思呢,我们不是把红黑树的节点加上除红与黑的另一种颜色,这里只是一种假设,我们认为我们当前指向它,因此空有额外一种黑色,可以认为它的黑色是从它的父节点被删除后继承给它的,它现在可以容纳两种颜色,如果它原来是红色,那么现在是红+黑,如果原来是黑色,那么它现在的颜色是黑+黑。有了这重额外的黑色,原红黑树性质5就能保持不变。现在只要恢复其它性质就可以了,做法还是尽量向根移动和穷举所有可能性。"--saturnman。


引入了这个新概念之后,那么现在假设性质5已经满足了,虽然实际上不满足,但是我们可以利用它是满足的条件,对树进行各种旋转,直到树达到我们所需要的形状,最后再修复性质5。

各个情况分析:

首先没有列出在算法导论给出的Case 1- Case 4中的两种情况:

a)当前节点是红+黑色

解法,直接把当前节点染成黑色,结束此时红黑树性质全部恢复。

b)当前节点是黑+黑且是根节点, 解法:什么都不做,结束。


这两种情况因为直接不满足while 的条件直接退出了while循环,思考一下很容易得出a)b)两种处理没有任何问题。

真正麻烦的是以下四种:

删除修复情况1:当前节点是黑+黑且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)

删除修复情况2:当前节点是黑加黑且兄弟是黑色且兄弟节点的两个子节点全为黑色

删除修复情况3:当前节点颜色是黑+黑,兄弟节点是黑色,兄弟的左子是红色,右子是黑色

删除修复情况4:当前节点颜色是黑-黑色,它的兄弟节点是黑色,但是兄弟节点的右子是红色,


删除修复情况1:当前节点是黑+黑且兄弟节点为红色(此时父节点和兄弟节点的子节点分为黑)。

解法:把父节点染成红色,把兄弟结点染成黑色,之后重新进入算法(我们只讨论当前节点是其父节点左孩子时的情况)。此变换后原红黑树性质5不变,而把问题转化为兄弟节点为黑色的情况(注:变化前,原本就未违反性质5,只是为了把问题转化为兄弟节点为黑色的情况)。即如下代码操作:

  1. //调用RB-DELETE-FIXUP(T, x) 的1-8行代码  
  2.  1 while x ≠ root[T] and color[x] = BLACK  
  3.  2     do if x = left[p[x]]  
  4.  3           then w ← right[p[x]]  
  5.  4                if color[w] = RED  
  6.  5                   then color[w] ← BLACK                        ▹  Case 1  
  7.  6                        color[p[x]] ← RED                       ▹  Case 1  
  8.  7                        LEFT-ROTATE(T, p[x])                    ▹  Case 1  
  9.  8                        w ← right[p[x]]                         ▹  Case 1  
经过这一段处理,兄弟节点变成了黑色,X节点自己也是黑色,那么就进入了Case 2 - Case 4中的一种了。这个变换的目的仅仅如此。

删除修复情况2:当前节点是黑加黑且兄弟是黑色且兄弟节点的两个子节点全为黑色。

解法:把当前节点和兄弟节点中抽取一重黑色追加到父节点上,把父节点当成新的当前节点,重新进入算法。(此变换后性质5不变),即调用RB-INSERT-FIXUP(T, z) 的第9-10行代码操作,如下:

  1. //调用RB-DELETE-FIXUP(T, x) 的9-11行代码  
  2. 9                if color[left[w]] = BLACK and color[right[w]] = BLACK  
  3. 10                   then color[w] ← RED                          ▹  Case 2  
  4. 11                        x p[x]                                  ▹  Case 2  
这一段操作之后,实际上就是向上走了,这就是这个自底向上的删除名字由来,经过Case2的处理,有概率直接完成修复,条件就是Case2处理前,X的父亲节点P是红色的,Case2 之后,X指向了原来红色的P,那么这个时候就不满足while的循环条件了,将X染成黑色,就真正修复了红黑树的性质。

如果还不满足呢?

那么此时的兄弟节点W,就要重新赋值了,再看看属于以上情况的哪一种,有可能情况2循环,直到达到根节点,此时才是完成了整个修复,能达到情况2循环的,应该是完全二叉树,在脑中模拟这个过程非常之烧脑,建议假设新的X指向了红色,一切OK。或者假设只有几个节点,立刻完成。


删除修复情况3:当前节点颜色是黑+黑,兄弟节点是黑色,兄弟的左子是红色,右子是黑色。

解法:把兄弟结点染红,兄弟左子节点染黑,之后再在兄弟节点为支点解右旋,之后重新进入算法。此是把当前的情况转化为情况4,而性质5得以保持,即调用RB-INSERT-FIXUP(T, z) 的第12-16行代码,如下所示:

  1. 12                   else if color[right[w]] = BLACK    
  2. 13                           then color[left[w]] ← BLACK          ▹  Case 3    
  3. 14                                color[w] ← RED                  ▹  Case 3    
  4. 15                                RIGHT-ROTATE(T, w)              ▹  Case 3    
  5. 16                                w ← right[p[x]]                 ▹  Case 3    
  6. 17                         color[w] ← color[p[x]]                 ▹  Case 4    
  7. 18                         color[p[x]] ← BLACK                    ▹  Case 4    
  8. 19                         color[right[w]] ← BLACK                ▹  Case 4    
  9. 20                         LEFT-ROTATE(T, p[x])                   ▹  Case 4    
  10. 21                         x ← root[T]                            ▹  Case 4 
此时处理完情况3,就自动进入情况4了。下面就和情况4和起来一起说。

删除修复情况4:当前节点颜色是黑-黑色,它的兄弟节点是黑色,但是兄弟节点的右子是红色,兄弟节点左子的颜色任意。

解法:把兄弟节点染成当前节点父节点的颜色,把当前节点父节点染成黑色,兄弟节点右子染成黑色,之后以当前节点的父节点为支点进行左旋,此时算法结束,红黑树所有性质调整正确,即调用RB-INSERT-FIXUP(T, z)的第17-21行代码,如下所示:

  1. //调用RB-DELETE-FIXUP(T, x) 的第17-21行代码  
  2. 17                         color[w] ← color[p[x]]                 ▹  Case 4  
  3. 18                         color[p[x]] ← BLACK                    ▹  Case 4  
  4. 19                         color[right[w]] ← BLACK                ▹  Case 4  
  5. 20                         LEFT-ROTATE(T, p[x])                   ▹  Case 4  
  6. 21                         x ← root[T]                            ▹  Case 4 
显然情况4处理完成之后,X赋值为根节点,将自动退出while循环,再将根设置为黑色,此时也代表修复完成了。

所以总得来时,有以下几条路线完成修复:

路线1:情况1->情况2,X指向了红色了父亲节点,然后退出循环,完成修复。

这里不存在情况1->情况2,然后情况2循环的可能,为什么不可能,大家可以自行画图尝试,我在这里给大家一个提示,能满足情况1进入情况2 的,被删除的Y一定是黑色的叶子节点,即这里的X已经是NullNode了,NullNode满足Black与非根节点,进入循环。

路线2:情况2->情况1->情况3/4,X指向了红色了父亲节点,然后退出循环,完成修复。

这个条件达成也非常难,想了大半天,发现不存在直接情况1->情况3/4,必须由情况2引导过去。

路线3:情况2->情况2->...->情况2,X指向了根节点,然后退出循环,完成修复。

这种情况下,基本上只有完全二叉树满足,很麻烦。

路线4:情况3->情况4,X指向了根节点,然后退出循环,完成修复。

这几种情况前面可能的引导情况我就不再去思考了,太累了,大家感兴趣可以自行思考怎样删除能到达这条路线

路线5:情况4,X指向了根节点,然后退出循环,完成修复。

这种情况或许也有引导,不过是在没力气再去想了。


两种删除对比:

综上所述,如果遇上了删除的是黑色的节点,那么就会有一个非常复杂的情况,遇上情况2时,还需要递归往上前进,因此一定需要有父亲指针。并且整个删除修复过程过于复杂,引入了新的概念,不利于理解。

自底向上删除的缺点:

以上4种情况就概括了所有的删除遇上黑色节点时,要处理的的情况。情况似乎和插入的情况差不多,但是缺点更加明显:

1. 加入了父亲指针之后,结构体变大,并且由于要处理的情况复杂,编码将更困难。

2. 要处理的情况太多,加上镜像模式过于复杂。

3. 太难理解,整个化解的过程也没有一个明确的指导思想,不知道为什么要从情况3变化到情况4,只知道这么做就行了,不利与以后自行推导变化过程。

自顶向下删除的改进:

使用自顶向下,思路完全不同,我们在这里害怕的就是删除了黑色的节点,然后破坏了红黑树的性质。那么改进方案就是,让被删除的节点变成红色,只需要安心删除就成。

1. 无需添加父亲指针

2. 编码简单

3. 推导思路明确,就是使得当前判断的节点变成红色


自顶向下的实现方法:

删除的时候要避免出现破坏树的性质,就必须要避免删除黑色的节点,就只需要一个办法:

让当前被访问的节点永远都是红色,需要删除时即可直接删除。


自顶向下删除实现:

首先再贴上参考文献:http://www2.ee.ntu.edu.tw/~yen/courses/ds-03/Red-Black-Trees-top-down-deletion.pdf

这篇文献中比《数据结构与算法分析》一书中更详细的介绍了如何把当前访问的X变成红色,但是并没有说清楚如何进行删除,该文献举了三个删除的例子,但是都不是典型的情况,而是最简单情况下的做法,并且example 2 的处理还出错了。

examp2 中,不能把根节点15修改成红色,否则,如果要删除的节点正好是根节点的话,处理的结果会出错。


首先介绍删除的思路:

1)带有两个儿子的节点,用右子树最小节点代替(将右子树最小节点内容放入此节点),删除该最小节点(此节点必然最多有一个儿子);

2)带有一个右儿子(右子树),以1)方式删除

3)带有一个左儿子(左子树),用左子树最大节点代替(将左字数最大节点内容放入此节点),删除该最大节点。


这里的做法与算法导论不同,这里删除的节点都是在叶子节点上完成的,这样可以让被删除的点变成红色时,可以直接进行删除。

那么当下降到树的中间(非叶子节点时),如果遇到了要被删除的元素,就左子树的最大节点,或者右子树的最小节点替换(可能进行不止一次,直到达到叶子节点),然后继续让X变成红色往下前进。


变成红色的方法:

我们定义几个全局变量:X当前节点,P父亲节点,T兄弟节点,GP祖父节点。

首先我们引入一个状态(文献中有step1,但是step1实际上可以利用开始的小技巧合并到step2中):

Step2:X为黑色,P为红色,T为黑色

这个状态与文献中的状态名字一样,便于大家与文献对照,其实我更愿意把它叫做StepRead,这个状态就是进入删除的while循环的初始状态,如果树的根存在,这个状态一定会满足。每次往下前进时,遇到各种不同的情形,经过不同的变换,最后又回到Step2.

step2的详细分类:Step2A,Step2B

处理完这两种情况就达到了当前节点X变成红色,然后判断该红色节点是不是要找的节点,不是就继续下降搜索,更新X,P,T,GP,回到了step2,。

注:存在一种特殊的情况,在step2B时需要转换成Step2A的情况。

Step2A:X有两个黑色的孩子(不存在X只有一个黑色的孩子,另一个指向NullNode的情况,这样就不满足性质5,另外,到达叶子节点后,一定属于Step2A,因为NullNode为黑色)

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第6张图片

这里我们总是假设X是P的左孩子,T是右孩子,以下所叙述的情况存在镜像的模式。处理方式相同,不在此赘述。

Step2A由可以分成三种情况:Step2A1,Step2A2,Step2A3.。

Step2A1:兄弟T有两个黑色的孩子

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第7张图片

左图是变化之前,右图是变化之后。白色代表红色节点。

处理方式是,P,X,T节点变色,X,T变成红色,P变成黑色。

在此可见,经过Step2A1的处理,我们已经把当前节点变成了红色。



Step2A2:兄弟T有一个红色的左孩子

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第8张图片

这里处理的目的,还是使得X节点变成红色,并且变成红色后不违背红黑树的性质4,5.

处理方式为:X,P变色,X变成红色,P变成黑色,然后对P进行右左旋转,即先对T右旋,再对P左旋。

此时如右图所示,我们已经得到了红色的X节点

Step2A3.:兄弟T有一个红色的右孩子,或者两个红色的孩子(兄弟T有两个红色孩子的情况分类进Step2A2或者Step2A3都可以,在这里我们放入Step2A3.)

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第9张图片

处理方式:X,P,T,R,全部变色,然后对P进行左旋转。

此时得到了红色的X节点。

Step2A(1,2,3)完成之后,我们就已经得到了红色的当前节点X。现在我们对X进行判断:

不是要删除的节点:如果X是不是要删的节点,那么我们继续下降。此时P变成现在的X,X和T根据下降的位置分配。此时回到了Step2。

是要删除的节点:那么我们在这里判断X是不是叶子节点(因为NullNode是黑色的,所以叶子节点满足Step2A的要求),如果是的,直接删除这个红色的节点就可以。如果不是,那么用它右子树上最小的值代替X节点中的值,不改变节点的颜色,把要删除的值设置为X中的新值,然后往右下降。此时父亲节P点是刚刚被替换了值的X节点并且为红色,新X是P的右子树,T是P的左子树。此时,又回到了Step2。


Step2B:X至少有一个红色的孩子

我们先来回顾此时的状态:P是红色,X与T是黑色。这是Step2的初始状态。我们要处理的目的是让X变成红色,或者至少经过变换,可以回到Step2开始的状态。

在这里,我们不同与Step2A的做法——先把X变成红色再判断是否要找的值。

我们在这里先判断X是否是是要寻找的值:

如果X不是:这是容易说清楚的状况,目的还是让X变成红色

因为X是不要寻找的值了,所以让它变成红色没有意义,我们选择直接下降,这时X有可能落在红色的孩子上,也可能落在黑色的孩子上。

(1)落在红色的孩子上:

这里就好比达到了Step2A的处理结果,让X变成了红色(并且是一个新的X),我们在这里进行判断,是否是要寻找的值。处理方式与Step2A完成之后一致,我在这里再列出来一次。

不是要删除的节点:如果X是不是要删的节点,那么我们继续下降。此时P变成现在的X,X和T根据下降的位置分配。此时回到了Step2。

是要删除的节点:那么我们在这里判断X是不是叶子节点,如果是的,直接删除这个红色的节点就可以。如果不是,那么用它右子树上最小的值代替X节点中的值(一定存在右子树),不改变节点的颜色,把要删除的值设置为X中的新值,然后往右下降。此时父亲节P点是刚刚被替换了值的X节点并且为红色,新X是P的右子树,T是P的左子树。此时,又回到了Step2。

(2)落在了黑色的节点上:

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第10张图片《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第11张图片

处理方式:P,T变色,然后对P进行做旋转,设置T指向P的右孩子。回到Step2初始状态

这一步处理,大家可能会疑惑,这样没吧把X变成红色啊,这不是问题,这样做的目的是把状态又拉回到了Step2。然后从新进入Step2A,或者Step2B,变成了红色之后,再对X进行判断。

如果X是要寻找的值:

这里的核心处理方式其实与之前相同,大家可能已经猜到了。

因为X节点有红色的子节点,所以不可能是叶子节点了,并且X是黑色的节点,所以一定要用叶子节点来替换它。

如果右节点存在,则使用右子树中的最小值来代替它,把要删除的值改为右子树上的最小值,然后向右下降,X,P,T对应更新。

如果右节点不存在,则使用左子树上的最大值代替它,删除值改为左子树最大值,然后向左下降,X,P,T对应更新。

完成了这一步,我们就好比把X当做了不是要删除的值,并且完成了下降处理。

下降之后,X指向的新值,可能就是红色,或者是黑色了。接下来的处理方式就如同上面所说的处理方式:(1)和(2)了。

到这里,我们就已经完成了把节点当前节点变成红色,并且达到叶子节点删除的过程(删除节点文献中设置为Step3,其实没有必要拿出来)。

Step4:将根节点染成黑色

因为在刚刚开始的变换时,我们把根变成了红色,最后需要变回来。

启动删除:

这里的问题就是如何设置,使得进入程序时就符合Step2的要求。这里就是设置一个黑色的NullNode以及黑色的假根T的妙处了:

我们这样处理:

P指向假根T(此时假根T理应是红色,但是我们这里不使用假根的颜色,所以假设它是红色,满足Step2的初始状态就可以了),X指向P的右子树,即真正的根。兄弟BRO指向P的左子树(编码的时候不再用T代表兄弟了,T代表树的假根,BRO代表兄弟,便于理解代码)。

这样,符合Step2的初始状态了,可以顺利进入Step2,然后进行处理。要设置的一个异常处理就是,如果先进入了Step2A1,注意不要把Null节点改变成红色了。

	X= T->right; 
	BRO = T->left;
	GP = GGP = P = T;

这段代码处理Step2A1,并且包含了异常处理

			if(BRO->left->color == Black && BRO->right->color == Black)
			{
				/*step 2A1*/
				P->color =Black;
				X->color = Red;
				if(BRO != NullNode)
					BRO->color = Red;
			}

到这里,我们就彻底说清楚如何自顶向下删除节点了。

自顶向下删除流程图:

上面说的过程虽然已经说清楚,但是肯定不如流程图明确,为了从理论到实现,我画出来了流程图,然后再进行编码:

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第12张图片

有了这个流程图,我们就可以轻易的编码实现了。

编码实现:

删除主循环:

/*删除节点*/
RedBlackTree Delete(ElementType item, RedBlackTree T)
{
	X= T->right; 
	BRO = T->left;
	GP = GGP = P = T;

	while(X != NullNode)
	{
		/*Setp2,又分成2A或者2B*/
		/*Step 2A*/
		if(X->left->color == Black && X->right->color == Black)
		{
			if(BRO->left->color == Black && BRO->right->color == Black)
			{
				/*step 2A1*/
				P->color =Black;
				X->color = Red;
				if(BRO != NullNode)
					BRO->color = Red;
			}
			else 
				/*step 2A23*/
				solveStep2A23(); 
			
			/*完成X染色为红色,父亲节点变为黑色*/
			if(X->Elememt == item)
				/*完成前进工作,或者完成删除,以X是否为NullNode区分*/
				item = findItem(item);
			else	
				/*没有找到节点时前进,然后进入step2*/
				normalDown(item);

		}
		/*step 2B*/
		else
		{	
			if(X->Elememt != item)
				/*step 2B,正常下降*/
				normalDown(item);
			
			else
				item = findItem(item);
			
			/*如果已经完成了删除,则X变成了NullNode*/
			if(X == NullNode)
				break;

			/*没有完成删除,则此时已经完成了下降*/
			if(X->color == Black)
				/*兄弟节点必然是红色,进行旋转,旋转之后X变成黑色,BRO变成黑色,P变成红色,
				回到Step2,再对X进行变换成红色,变成红色之后,再进行判断X是否是要找的节点*/
				solve2B();
			else if(X->Elememt != item)
				/*红色继续下降,X与BRO将都会是黑色*/
				normalDown(item);
			else
				item = findItem(item);
		}
	}

	/*Step4, 3已经在上面完成完成*/
	T->color = Black;
	T->right ->color = Black;

	return T;
}

在这里,Step2B的流程似乎与我列出来的流程图不太一致,这是因为,我在编码findItem函数时,寻找的子树的最小值或者最大值时,如果找的的最小值或者最大值节点是红色的节点,我就直接删除了该节点并设置X指向NullNode,因此在中间加入了判断X是否是NullNode并跳出循环的判断。原因是我没有保证一定在叶子节点上删除,所以如果在有一个孩子的节点上删除了,我必须要发现这个情况,并停止下降过程,

建议大家还是按照流程图来编码。我这样编码打破了程序的结构,不容易理解代码了。这是编码过程中忽视的部分。

解决Step2A2/3:

void solveStep2A23()
{
	/*兄弟在右边*/
	if(P->left == X)
	{
					
		if(BRO->left->color == Red)
		{
			/*step 2A2*/
			P->color = Black;
			X->color = Red;
			P->right =  rightSingleRotate(BRO);
			if(GP->left == P)
				GP->left = leftSingleRotate(P);
			else
				GP->right = leftSingleRotate(P);
		}
		else
		{
			/*step 2A3*/
			X->color = Red;
			P->color = Black;
			BRO->color = Red;
			BRO->right->color = Black;

			if(GP->right == P)
				GP->right = leftSingleRotate(P);
			else
				GP->left = leftSingleRotate(P);
		}
	}
	/*兄弟在左边*/
	else
	{
		if(BRO->right->color == Red)
		{
			/*step 2A2*/
			X->color = Red;
			P->color = Black;
			P->left = leftSingleRotate(BRO);
			if(GP->left == P)
				GP->left = rightSingleRotate(P);
			else
				GP->right = rightSingleRotate(P);
		}
		else
		{
			/*step 2A3*/
			X->color = Red;
			P->color = Black;
			BRO->color = Red;
			BRO->left->color = Black;

			if(GP->right == P)
				GP->right = rightSingleRotate(P);
			else
				GP->left = rightSingleRotate(P);
		}
	}

}

使用叶子节点替换函数:

lementType findItem(ElementType item)
{
	ElementType temp;
	RedBlackTree ToDelete;
	/*如果是要寻找的点,先判断是否是叶子节点,不是再通过替换节点删除*/
	if(X->left == NullNode && X->right == NullNode)
	{
		if(P->right == X)
			P->right = NullNode;
		else
			P->left = NullNode;

		free(X);
		X = NullNode;
		temp = item;
	}
	else
	{
		/*不是叶子节点*/
		if(X->right != NullNode)
		{
			/*从右边寻找一个最小的节点放在X的位置上*/
			ToDelete = find(item, X->right);
			X->Elememt = ToDelete->Elememt;
			temp = ToDelete->Elememt;
			if(ToDelete ->color == Red)
			{
				/*如果找的节点是红色,可以直接删去该节点*/
				X->right = DeleteNode(ToDelete, X->right);
				X = NullNode;
			}
			else
			{
				/*A2中:往右前进,此时新的X,一定是黑色,兄弟BRO也一定是黑色,此时就回到了step2
				  B2中:右前进,此时新的X,可能是红色也可能是黑色,如果是红色的话,一定不是ToDelete节点,因为在上面已经排除过了*/
				GP = P; P= X; BRO = P->left;	
				X = X->right;
			}
		}
		else
		{
			/*从左边寻找一个最大的节点放在X的位置上*/
			ToDelete = find(item, X->left);
			X->Elememt = ToDelete->Elememt;
			temp = ToDelete->Elememt;
			if(ToDelete ->color == Red)
			{
				/*如果找的节点是红色,可以直接删去该节点*/
				X->left = DeleteNode(ToDelete, X->left);
				X = NullNode;
			}
			else
			{
				/*往左前进,此时新的X,一定是黑色,兄弟BRO也一定是黑色,此时就回到了step2
				B2中:右前进,此时新的X,可能是红色也可能是黑色,如果是红色的话,一定不是ToDelete节点,因为在上面已经排除过了*/
				GP = P; P= X; BRO = P->right;
				X = X->left;
			}
						
		}

	}

	return temp;
}
左右子树最小值最大值,如果是红色的删除代码:

RedBlackTree DeleteNode(RedBlackTree target, RedBlackTree T)
{
	RedBlackTree origin = T;
	RedBlackTree Parent 
		;
	while(T != target)
	{
		Parent = T;
		if(target->Elememt < T->Elememt)
			T= T->left;
		else
			T = T->right;
	}

	if(T == origin)
	{
		RedBlackTree temp;
		if(T->right != NullNode)	
			temp =T->right;
		else
			temp = T->left;

		free(T);	
		return temp;
	}

	if(Parent->right == T)
	{
		if(T->right != NullNode)
			Parent->right = T->right;
		else
			Parent->right = T->left;
	}
	else
	{
		if(T->right != NullNode)
			Parent->left = T->right;
		else
			Parent->left = T->left;
	}
	free(T);
	return origin;

}

解决Step2B(2)代码:
void solve2B()
{
	BRO->color = Black;
	P->color = Red;
	if(P->left == X)
	{
		if(GP->left == P)
			GP->left = leftSingleRotate(P);
		else
			GP->right = leftSingleRotate(P);
		BRO = P->right;
	}
	else
	{
		if(GP->left == P)
			GP->left = rightSingleRotate(P);
		else
			GP->right = rightSingleRotate(P);

		BRO = P->left;
	}

	
}

到这里所有的代码都已经编码实现了,现在只差测试了:

测试结果:

先插入一段数据:

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第13张图片

然后逐步删除,直到删除所有的点:

部分删除结果:

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第14张图片

部分删除结果:

《数据结构与算法分析》详细对比自顶向下与自底向上红黑树——C实现自顶向下插入与删除_第15张图片

总结:

红黑树这一部分确实使非常的难,国庆期间差不多花了两天时间实现代码,又花了两天实现才写成了这篇博客。不过学习完红黑树,我想所有其他的数据结构基本就不成问题了。学习到这里,整本《数据机构与算法分析》也就只剩下几个结构了,再花两天看一看,立刻进入下一个阶段的学习吧!加油!




你可能感兴趣的:(数据结构)