红黑树的奇妙之旅——从零到一

红黑树的定义和性质

红黑树是一种含有红黑结点并能自平衡的二叉搜索树。它在每个结点上增加了一个存储位来表示结点的颜色,可以是RED或者BLACK。通过任何一条从根到叶子的简单路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,从而达到近似平衡。。

为了便于理解,设置树中每个结点包含5个属性:color,key,left,right,p。如果一个结点没有子结点或者父结点,则该结点相应指针属性为NIL。我们可以把这些NIL视为二叉搜索树的叶结点(外部结点),而带关键字的结点则视为树的内部结点。。

一颗红黑树是满足以下红黑性质的二叉搜索树:

  1. 每个结点或是红色的,或是黑色的;
  2. 根结点是黑色的;
  3. 每个叶结点(NIL)是黑色的;
  4. 如果一个结点是红色的,则它的两个子结点都是黑色的;
  5. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

下图显示了一个完整红黑树的例子:
在这里插入图片描述
我们通常将注意力集中在红黑树的内部结点中,因为它们存储了关键字的值。
下图我们给每一个结点标出它的黑高:
在这里插入图片描述
从某个结点 x 出发(不含该结点)到达一个叶结点(黑色NIL结点)的任意一条简单路径上的黑色结点个数称为该结点的黑高(black-height)。根据上述性质5,黑高的概念是明确定义的,因为从该结点出发的所有下降到其叶结点的简单路径的黑结点个数都相同。定义红黑树的黑高为其根结点的黑高。。

红黑树维持红黑性质以及自平衡的“秘诀”

红黑树在它的一系列操作(如插入结点,删除结点)中是可能违反它的红黑性质的,甚至导致树的不平衡,那么它是咋样做到维持平衡以及红黑性质的呢??

秘诀就是 旋转 加上 变色 。。。下面我们一起来看看它的秘诀吧!!!

旋转

旋转是一种能保持二叉搜索树性质的对树的局部操作。分为 左旋右旋

  • 左旋 以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。
  • 右旋 以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。

看图:
在这里插入图片描述
下图给出了一个LEFT-ROTATE操作修改二叉搜索树的例子(红黑树中的使用也是一样的):

LEFT-ROTATE伪代码(RIGHT-ROTATE操作的伪代码与LEFT-ROTATE的伪代码是对称的):

LEFT-ROTATE(T,x)
	y = x.right
	x.right = y.left    //使 x 结点的右子树变为 y 结点的左子树
	if y.left != NIL
		y.left.p = x
	y.p = x.p
	if x == T.root   //当 x 结点是根结点时
		T.root = y
	elseif x == x.p.left
		x.p.left = y
	else  
		x.p.right = y
	y.left = x   //使 y 的左子树变为 x
	x.p = y

在这里插入图片描述
通过这样的实例看起来旋转是不是简单了许多呢!!!

变色

变色相对而言就更简单了,直接将结点的颜色由红变黑或由黑变红即可。

至此,对于旋转和变色我们都有了一定的了解,接下来,我们就进入到实际的情况中看看它们究竟是如何发挥作用的!!!

红黑树结点的插入和删除

无论是插入或是删除操作过程中,都有可能引起红黑树的树结构发生变化,红黑树必须要保证红黑性质能够继续保持,以及二叉搜索树的自平衡性质。

首先,先简单介绍一下红黑树的自平衡以及维持红黑性质:红黑树首先是一颗二叉搜索树,在插入删除过程中也就需要保证平衡,而红黑树的高(这里是指黑高,红黑树以黑高为树高)的发生变化的情况,也就仅有插入或者删除了黑色结点。显然,这个同时也会引起红黑树红黑性质被破坏。。。下面我们将一同处理这个两个过程(自平衡以及维持红黑性质)。。。。。。

在了解红黑树的插入与删除之前我们先看看红黑树的查找(毕竟,树的插入和删除都离不开查找操作):

查找
  1. 从根结点开始查找,把根结点设置为当前结点;
  2. 若当前结点为空,返回null;
  3. 若当前结点不为空,用当前结点的key跟查找结点的key作比较;
  4. 若当前结点的key等于查找结点的key,那么该key就是查找目标,返回当前结点;
  5. 若当前结点的key大于查找结点的key,把当前结点的左子结点设置为当前结点,重复步骤2;
  6. 若当前结点的key小于查找结点的key,把当前结点的右子结点设置为当前结点,重复步骤2;

整个查找过程与二叉搜索树的查找基本无异!!

红黑树总能保持黑色完美平衡,所以它的查找最坏时间复杂度为O(2lgN),也即整颗树刚好红黑相隔的时候。

插入

插入操作包括两部分工作:一查找插入结点的位置;二插入后红黑树整体维持红黑性质且自平衡。

首先,先看查找结点插入的位置:即查找要插入结点的父结点
有了上面结点查找的分析,查找结点插入的位置不会太难,结点插入的位置找到了,只要将结点的 color 属性赋值 RED ,然后放入对应的插入位置,那么就可以进行自平衡了。但是,插入结点为什么要是红色的呢???

理由很简单,为了减少维持红黑性质以及自平衡操作的次数。(红色在父结点(如果存在)为黑色结点时,红黑树的黑色平衡没被破坏,不需要做维持操作;但如果插入结点是黑色,那么插入位置所在的子树黑色结点总是多1,必须做维持红黑性质以及自平衡操作)

下面是插入操作的伪代码:

插入操作伪代码(RB-INSERT):

//查找插入结点z的位置
RB-INSERT(T,z)
	y = NIL
	x = T.root
	while x != NIL      //查找插入结点 z 的位置
		y = x
		if z.key < x.key
			x = x.left
		else
			x = x.right
	z.p = y    // y 结点为 z 结点的父结点
	if y == NIL             //要插入的位置为根结点,且树为空树
		T.root = z
	elseif z.key < y.key    //要插入的位置为 y 结点的左孩子
		y.left = z;
	else                    //要插入的位置为 y 结点的右孩子
		y.right = z
	z.left = NIL              
	z.rigth = NIL
	z.color = RED
	RB-INSERT-FIXUP(T,z)	

接下来,就要对插入结点后的红黑树进行维持红黑性质以及自平衡(RB-INSERT-FIXUP):

相对于查找插入的位置,维持红黑性质之前我们还需要对该位置进行分析(设要插入结点为z,):

根据红黑树的5条性质来看:
性质1和性质3继续成立,因为新插入的红结点的两个子结点都是NIL(黑色叶子结点),而插入红结点的位置必然是当前红黑树的一个叶子结点位置。
对于性质5,即从一个指定结点开始的每条简单路径上的黑色结点的个数都是相等的,也会成立,因为插入结点 z 本身是红色的,它代替了一个黑色叶子结点,且它自身带着黑色的叶子结点(NIL)。
而对于性质2和性质4,都有可能被破坏:如果 z 是根结点,则破坏了性质2;如果 z 的父结点是红色的,则破坏了性质4。

分析破坏红黑性质的情况
当性质2被破坏时,由于要插入的位置是根结点(红黑树插入前为空树),故不存在性质4同时被破环的可能;同样的,性质4被破坏时,也没有其他红黑性质被破坏。

性质2被破坏的情况较为简单,仅仅是涉及根结点,因此仅需要在维持红黑性质操作中保证根结点为黑色即可;

但是性质4被破环的情况就显得较为复杂:
首先要明白的是如果插入位置的父结点为红结点,那么该父结点不可能为根结点,所以插入结点总是存在祖父结点 。这点很重要,因为后续维持红黑性质需要旋转操作,而旋转操作肯定需要祖父结点的参与。接着继续分情况分析:

由于 z 的父结点是 z 的祖父结点的左结点或右结点的情况修改操作是对称的,故针对其中一种情况进行分析:

取 z 结点是 z 结点祖父结点的左孩子为例:

情况1:z 父结点的兄弟结点(叔结点)是红色的。
由 z 的叔结点和父结点都是红色的,可知,z 的祖父结点必然是黑色的(因为不可以同时存在两个相连的红结点),如下图(z为P的右孩子也是一样的):
在这里插入图片描述
至此,PP结点的颜色修改为红色,P和U修改为黑色,那么 z 结点位置的红黑性质就已经维持好了;但是,PP结点修改了颜色,那么它同样也需要进行维持红黑性质的检测与操作。即接下来将PP结点作为插入结点继续做维持红黑性质且自平衡操作,直到整个红黑树符合红黑性质且平衡为止。

情况2:叔结点不存在或是黑色的,并且插入结点 z 是一个右孩子。
情况3:叔结点不存在或是黑色的,并且插入结点 z 是一个左孩子。
如下图:
在这里插入图片描述
通过上述的旋转变色操作,性质4也能得到保证。

至此,z 结点是 z 结点祖父结点的左孩子的情况下的维持红黑性质且自平衡操作也就完成了。

下面看红黑树整体维持红黑性质且自平衡操作的伪代码(RB-INSERT-FIXUP):

//维持红黑树红黑性质且自平衡
RB-INSERT-FIXUP(T,z)
	while z.p.color == RED       //如果 z 结点的父结点是红色(解决破坏性质4的情况)
		if z.p == z.p.p.left        //如果 z 结点的父结点是它父结点的左孩子
			y = z.p.p.left
			if y.color == RED       // 情况1
				z.p.color = BLACK
				y.color = BLACK
				z.p.p.color = RED
				z = z.p.p
			elseif z == z.p.right    // 情况2
				z = z.p
				LEFT-ROTATE(T,z)
			z.p.color = BLACK     // 情况3
			z.p.p.color = RED
			RIGHT-ROTATE(T,z.p.p)
		else     //如果 z 结点的父结点是它父结点的右孩子(操作对称)
			y = z.p.p.right
			if y.color == RED
				z.p.color = BLACK
				y.color = BLACK
				z.p.p.color = RED
				z = z.p.p
			elseif z == z.p.left
				z = z.p
				RIGHT-ROTATE(T,z)
			z.p.color = BLACK
			z.p.p.color = RED
			LEFT-ROTATE(T,z.p.p)
	T.root.color = BLACK    //解决可能破坏性质2的情况

总的看来,红黑树的插入过程都是自底向上处理,每一颗子树处理好了,那么整颗树自然也就处理好了!!!

删除

相对插入操作而言,红黑树的删除操作要稍微复杂些。

红黑树的删除操作也包括两部分工作:一查找目标结点;二删除后恢复红黑性质以及自平衡。查找目标结点显然可以复用查找操作,找到目标结点然后删除它;然而,删除了结点后我们还需要找结点来替代删除结点的位置,不然子树跟父辈结点断开了,除非删除结点刚好没子结点,那么就不需要替代。

替代的情况较好分析,可以分为三种情况:

  • 情况1:若删除的目标结点无子结点,则直接删除,无需替代;
  • 情况2:若删除的目标结点只有一个子结点,则直接用子结点替代删除的目标结点;
  • 情况3:若删除的目标结点有两个子结点,则用目标结点的后继结点(大于删除的目标结点的最小结点,一定在目标结点的右子树中)替代删除的目标结点结点。
    补充说明:对于情况3中的后继结点,它是大于目标结点的最小后代结点,也是目标结点的右子树种最左结点(其实,也可以用要删除的目标结点的前继结点来替代目标结点,但习惯上大多都是拿后继结点来替代)。

在删除结点的过程中,不得不提一个重要的思路删除的目标结点被替代后,在不考虑结点的结点值的情况下,对于树来说,可以认为删除的是替代结点。这个思路怎么理解呢??下面看图:
在这里插入图片描述
这样看来,替代的情况中看似复杂的情况3就可以简化为情况1,情况2进行考虑:

对于情况3,删除的目标结点用其后继结点(该后继结点肯定不存在左结点)替代,那么就可以看作要删除的是目标结点的后继结点(目标结点与其后继结点的结点值交换)。如果后继结点有右子结点,那么相当于转换为情况2,否则转换为情况1。

如此,替代操作也不是很难!!!!

接着需要考虑的就是恢复红黑树红黑性质了(如果在删除结点的过程中红黑性质有被破坏的话)。那么,什么情况下红黑树的红黑性质会遭到破坏呢???

很简单,**当被删除的结点的颜色是黑色的时候,红黑树的红黑性质必然遭到破坏,也只有在这种情况下,需要执行恢复红黑性质以及自平衡的操作!!**破坏的情况以及如何恢复且往下看。。

对于如果y是红色,《算法导论》是这么说的:

如果y是红色,当y被删除或者移动时,红黑性质仍然保持,原因如下:
1.树中的黑高没有变化
2.不存在两个相邻的红结点。因为y在树中占据了z的位置,再考虑到z的颜色,树中y的新位置不可能有两个相邻的红结点。另外,如果y不是z的右孩子,则y的原右孩子x代替y。如果y是红色,则x一定是黑色,因此用x替代y不可能使两个红结点相邻。
3.如果y是红色,就不可能是根结点,所以根结点仍旧是黑色。

这里先上删除结点以及替代过程的伪代码

替代过程(RB-TRANSPLANT):使用另一颗子树替换一棵子树并成为其双亲的孩子结点。如:当RB-TRANSPLANT用一颗以 v 为根的子树来替换一颗以 u 为根的子树时,u 的双亲就变为了结点 v 的双亲,并且最后 v 成为 u 的双亲相应的孩子。

替代过程(RB-TRANSPLANT)伪代码:

RB-TRANSPLANT(T,u,v){
	if u.p == NIL
		T,root = v
	elseif u == u.p.left
		u.p.left = v
	else
		u.p.right = v
	v.p = u.p
}

寻找树中结点值最小的结点(RB-TREE-MINIMUM)伪代码:

//x参数表示要查找以 x 结点为树根的树
//结果返回该树中结点值最小的结点
//目的是返回结点 x 的后继结点
RB-TREE-MINIMUM(x){
	while x.left != NIL
		x = x.left
	return x
}

删除结点操作(RE-DELETE)伪代码:

//删除结点z
RE-DELETE(T,z){
	y = z
	y-color = y.color          //记录要删除结点的颜色
	if z.left == NIL           //情况1和情况2:直接替代
		x = z.right
		RB-TRANSPLANT(T,z,z.right)
	elseif z.right == NIL
		x = z.left
		RB-TRANSPLANT(T,z,z.left)
	else                      //情况3
		y = RB-TREE-MINIMUM(z.right)      //  寻找后继结点,替代被删除结点
		y-color = y.color
		x = y.right
		if y.p == z       //处理替代结点的右子结点(替代结点必然不存在左子结点)
			x.p = y
		else
			RB-TRANSPLANT(T,y,y.right)
			y.right = z.right
			y.right.p = y
		RB-TRANSPLANT(T,z,y)
		y.left = z.left
		y.left.p = y
		y.color = z.color       //即仅目标结点z与后继结点的结点值发生变化,其他属性不变(包括颜色)----替代删除的过程
	if y-color ==BLACK       //只有被删除结点的颜色为黑色时,才会破坏红黑树的红黑性质,才需恢复红黑性质以及自平衡
		RB-DELETE-FIXUP(T,x)
}

接下来,我们便讲讲红黑树性质被破坏以及恢复和自平衡的事儿吧!!!

首先,如果 y 的颜色是黑色的,那么它被移除时就可能产生以下3个问题:

第一,如果 y 是原来的根结点,而 y 的一个红色孩子结点将成为新的根结点。这就违背了性质1。
第二,移除 y 后,若 x 和 x.p 都是红色的,则违背了性质4。
第三,在树中移除 y 将导致先前包含 y 的任何简单路径上的黑结点个数减1,因此,会使得 y 的任何祖先都不满足性质5。

然而,对于问题一(违背性质1)和问题二(违背性质4)解决起来都相当简单:若 y 是原来的根结点,那么只需要将 y 的那个新成为根结点的颜色改为黑色即可;若删除或者移动 y 后,若 x 和 x.p 都是红色的,那么同样只需要将 x 结点的颜色改为黑色即可。

最难啃的就是这个“问题三”了!!

对于问题三,有一个简便思路因为移除了 y (x的父结点),而且 y 是黑色的,那么为了保持 y 所在简单路径的黑色结点数目不变(即维持红黑性质5),考虑将 y 的黑色“下推”给 x (y的孩子结点),意思就是此时 x 带着两个 color 属性(可能是红黑,也可能是黑黑),这样考虑下,性质5也就成立了,但与此同时,性质1又被破坏了,因为 x 此时是双重颜色(双重黑色或者红黑色),那么我们接下来就只需要将 x 上多余黑色变换到一个红色结点(即将其红色改为黑色,而非"推")上,问题也就解决了!!!!
注:x 结点的color属性仍然为RED(如果 x 是红黑色)或者BLACK(如果 x 是双重黑色)!!!

按照这个思路,我们继续分析:

因为要将 x 的“多余的黑色”变换到一个红色结点(必然是 x 的先辈结点,想想为什么??文后评论处见答案)上,那么就得考虑 x 的兄弟结点。。。

下面分情况讨论:

情况1:x 的兄弟结点 w 是红色的
在这里插入图片描述如图,在对于 x.p 的左旋过程中,红黑树的黑高不受影响,也就不违反红黑树的任何性质。现在,x 的新兄弟结点就是旋转之前 w 的某个子结点,其颜色为黑色。这样,就将情况1转换为情况2,3或者4处理。。。

情况2:x 的兄弟结点 w 是黑色的,而且 w 的两个子结点都是黑色的
在这里插入图片描述
如图,w 的两个孩子结点都是黑色,w 也都是黑色的,所以从 x 和 w 上同时去掉一层黑色,使 x 只有一层黑色而 w 为红色。为了补偿一层黑高的损失,在原来是红色或者黑色的 x.p 上新增一层额外的黑色。通过将 x.p 作为新的 x 继续循环。
注意,如果通过情况1进入的情况2,则新结点 x 为红黑色的,因为原来的 x.p 为红色,新增一层额外的黑色后其属性color依旧为RED,循环终止(见下方伪代码),且在最后对带有额外一层黑色的红色结点 x 着为单一的黑色。(有点绕,总之就是原来的 x.p 是新的 x)

情况3:x 的兄弟结点 w 是黑色的,w 的左孩子是红色的,w 的右孩子是黑色的
在这里插入图片描述
w 是黑色的,w 的左孩子是红色的时,交换 w 与其左孩子 w.left 的颜色,然后进行左旋。整个过程中仍旧不违反红黑性质(每一条简单路径的黑高依旧不变)。现在 x 的新兄弟结点 w 是一个有红色右孩子的黑色结点,这样情况3也就转化了情况4。

情况4:x 的兄弟结点 w 是黑色的,且 w 的右孩子是红色的
在这里插入图片描述
在情况4中,通过修改颜色并对 x.p 进行一次左旋,可以去掉 x 的额外黑色,从而使它变成单重黑色,而且不破坏红黑树的任何性质。然后将 x 设置为根后,循环终止。。。

这样,红黑树删除过程中的恢复红黑性质及自平衡也就完成了。。。下面看恢复红黑性质自平衡的伪代码:

恢复删除结点后红黑树的红黑性质以及自平衡(RB-DELETE-FIXUP)伪代码:

RB-DELETE-FIXUP(T,x){
	while x != T.root and x.color ==BLACK
		if x == x.p.left
			w = x.p.right
			if w.color ==RED
				w.color =BLACK             //case1
				x.p.color = RED
				LEFT-ROTATE(T,x.p)
				w = x.p.right
			if w.left.color == BLACK and w.right.color == BLACK
				w.color = RED               //case2
				x = x.p
			else if w.right.color ==BLACK
				w.left.color = BLACK         //case3
				w.color = RED
				RIGHT-ROTATE(T,w)
				w = x.p.right
			w.color = x.p.color               //case4
			x.p.color = BLACK
			w.right.color = BLACK
			LEFT-ROTATE(T,x.p)
			x = T.root
		else (same as then clause with "right" and "left" exchanged)
	x.color = BLACK
}

呼!!!耗时良久,终于完成了红黑树的梳理,在此过程中,参考过另一篇关于红黑树的详解 参考文章,以及@算法导论(第三版)。。大家若有什么其他看法可以一起交流啊!!!!!!

发布了20 篇原创文章 · 获赞 10 · 访问量 1020

猜你喜欢

转载自blog.csdn.net/qq_42964349/article/details/104590834