红黑树学习笔记


前言

今天看了一早上的红黑树,唉,感觉天资愚笨,算法导论上的红黑树删除节点解释愣是没看懂,到头来还是参考了红黑树的学习与理解这个博客才大致理解了,现在记录一下加深一下印象。


一、红黑树是什么?

红黑树是一种特殊的二叉搜索树,能够有效的平衡每条路径的高度,没有一条路径的高度会比其它路径长出两倍,这样就能够确保在最坏情况下基本动态集合操作的时间复杂度为O(lgn)。为什么红黑树会有这样好的性质呢?这就要依赖于红黑树的定义了:
红黑树是满足以下五条性质的二叉搜索树:

  1. 每个节点的颜色为红色或者黑色
  2. 根节点是黑色的
  3. 叶节点NIL都是黑色的
  4. 红色节点的子节点都是黑色的
  5. 对每一个节点来说,从这个节点到其所有叶节点的路径上均包含相同的黑节点数目(黑高一样)

这里我们假设一个极端的红黑树,根节点的左子树都为黑节点n个,那右节点最高为2n。然后这里定义了一个哨兵节点T.nil,所有的NULL节点都使用T.nil来代替。这里个人觉得性质3用处不是很大,因为所有的NULL指针都被黑色的T.nil叶节点替代了,不用性质4的话似乎也能成立,在书中树的叶节点和根的父节点都被省略了。
红黑树示意图
是

二、插入删除操作

红黑树的查询操作和普通搜索树的查询操作是一样的,但是插入和删除操作就要在原有的操作上做进一步调整以满足红黑树的性质。

1.旋转

旋转操作可以在满足二叉搜索树的性质时调整树的局部调整操作。根据旋转的方向可以分为左旋和右旋。
左旋:对X节点左旋,即以X的右孩子Y为轴,将X节点转下来,变为Y的左孩子,简单记为左旋即把该节点变为左孩子。
在这里插入图片描述
右旋:对X节点右旋,即以X的左孩子Y为轴,将X节点转下来,变为Y的右孩子,简单记为右旋即把该节点变为右孩子。
就是把自己的左或者右孩子变为父节点,而自己变为左或者右节点,然后将剩下的子树拼接到自己身上,哪边缺就往那边补。

2.插入

对红黑树的节点插入操作可以分为两步

  1. 按照普通二叉搜索树的步骤插入节点z
  2. 将节点z的颜色置为red然后,调整为红黑树

对普通二叉搜索树的插入过程可以简单理解为:找到跟待插入节点z大小最接近的边缘节点然后把其作为父节点插入z。伪代码如下

def RB-INSERT(T, z):
	x = T.root
	y = T.nil  # 待插入的父节点
	while x != T.nil: #找到最接近
		y = x
		if z.key<x.key:
			x = x.left
		else
			x = x.right
	z.p = y
	if y == T.nil:
		T.root = z
	else if y.key > z.key
		y.left = z
	else
		y.right = z
	z.left = T.nil
	z.right = T.nil
	z.color = RED
	# 调整树结构
	RB-INSERT-FIXUP(T, z)

然后下一步就是稍复杂的树结构调整过程
要调整该二叉搜索树重新满足红黑树定义,我们要分析在末端插入一个红色的节点后什么性质被破环了,显然当树为空时 2.根节点为黑 被破环了,不为空时可能会因为4.父节点是红色破坏红色节点的孩子都是黑节点的性质。第一种情况直接将其染黑即可,现在主要分析第二种情况。按照书上的分析根据z.p是z.p.p的左节点还是右节点可以分为两种情况,这两种情况下又有三种情况,但这两种情况下操作是对称的。所以只分析z.p = z.p.p.left的情况。

情况1:z的叔节点为RED。这个也是第一次进入循环的的情况,因为如果叔节点为BLACK,那么原始的路径就不相等。这种情况处理很简单直接把父节点和叔节点染黑,z.p.p染红,把矛盾传递给z.p.p,把z.p.p作为新的z节点。
在这里插入图片描述
这样处理过后只有性质4可能被违反,其它性质都不会被违反。保持性的证明可以看书P178.

情况2:z的叔叔节点是黑色而且z是一个右孩子。情况1一直循环要不然解决问题,要不就会进入到情况2或者3中。z的叔叔节点y为黑色而且z是一个右孩子
在这里插入图片描述
这里我们需要将z和z.p做左旋操作,如上图所示,因为z,z.p都是红色节点,所以左旋之后并不会违反红黑树的其它性质,那么现在就进入了情况3.

情况3:z的叔叔z的叔叔节点是黑色而且z是一个左孩子。在这种情况下就可以完成调整工作了。
在这里插入图片描述
将z.p染黑,z.p.p染红,然后将z.p.p右旋。此时z.p为黑色,性质4得到满足,而且易于证明每个子树的黑高没有改变,性质5也得到保持,调整完毕。

然后给出插入调整RB-INSERT-FIXUP的伪代码

def RB-INSERT-FIXUP(T, z):
	while z.p.color == RED:
		if z.p == z.p.p.left:
			y == z.p.p.right
			if y.color == RED:
				z.p.color = B
				y.color = B
				z.p.p.color = R
			else
				if z == z.p.right:
					z = z.p
					LEFT-ROTATE(T, z)
				z.p.color = B
				z.p.p.color = R
				RIGHT-ROTATE(T, z.p.p)
		else:
			#对称的工作
	T.root.color = B

3.删除

红黑树的删除也可以分为两步

  1. 对普通二叉搜索树的删除基础上略微修改,记录结构上实际删除节点的信息
  2. 对删除得到的二叉搜索树做调整,使其重新符合红黑树的性质

二叉搜索树对一个节点删除的大概流程是,如果z是一个单子树节点,那么只要将孩子子树替换掉z子树就可以。否则找到其后继节点y,y一定是没有左孩子,这时候将其右孩子替换到y的位置,然后y再替换到z的位置完成删除。在红黑树删除的大致流程上是一样的,但是因为红黑树具有color属性和必须遵守的性质,还需要做一些改进,需要记录一些额外的信息。下面放上删除的伪代码

def RB-DELETE(T, z)
 	 y = z;
 	 y-original-color = y.color;
  	if z.left == T.NIL
    	x = z.right;
    	RB-TRANSPLANT(T, z, z.right);
  	else if z.right == T.NIL
    	x = z.left;
    	RB-TRANSPLANT(T, z, z.left);
  	else 
  		y = TREE-MINMUM(z.right)
    	y-original-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;
  	if y-original-color == black
    	RB-DELETE-FIXUP(T, x)

可以看到这里记录了y的color信息,和y的子树x的信息,为什么记录这些信息呢,我们可以分析一下删除的过程。
当z是单子树节点时,令y(y的具体含义后面解释)指向z,x(具体含义先按下不表)指向y的唯一子树,然后将x代替z。
在这里插入图片描述
当z是双子树节点时,y指向其后继,x指向y的唯一右子树,然后x取代y的位置,y取代z的位置。
在这里插入图片描述
在这里插入图片描述

我们可以看到在逻辑上我们删除的是z节点,但是在实际的树结构上丢失的却是y节点,而顶替上的部分是y的右孩子x。这时候我们分析如果删除的y节点原本是一个RED节点,那么易于证明没有破坏红黑树的任何性质。但如果是一个BLACK节点,那么首先填上去的x的黑高肯定会少1,而且因为可能还会因为y.p.p是RED节点导致破坏性质4,如果y是根节点还可能会破坏性质2.所以RB-DELETE-FIXUP(T, x)中要调整这些问题。
首先如果x为红色,那么直接把x染黑就能解决所有问题。
现在考虑x为黑色时,造成的黑高少1问题。这里书上的解释我觉得很厉害,就是给x再加上一个黑色,此时x既不是BLACK也不是RED节点(但属性还是BLACK),是一个双重BLACK,然后不断的将额外的黑色上移,直到:

  1. x指向红黑节点,移除黑色
  2. x指向根节点,移除黑色
  3. 执行适当的旋转和重新着色,退出循环

这样就可以完成调整了。
具体的调整可以分8种情况,因为x = x.p.left和x=x.p.right的操作是对称的,所以实际上只有四种情况。
情况1. x的兄弟节点w为RED
在这里插入图片描述
此时将a染红,w染黑,然后将a左旋(可以理解为a,w先都染红,这时候旋转是不会引起黑高改变的,然后为了保持跟父子不能同时为红的性质,再将父节点染黑),这时不违反红黑树的其它性质,然后进入别的情况中。

情况2. x的兄弟节点w为BLACK,然后w的两个子节点都是黑色
在这里插入图片描述
这时将x和w同时减去一个黑色,为了保持黑高相同的性质,令父节点a增加一个黑色。x变为a,如果a原本为红色那么,结束,否则继续判断

情况3. x的兄弟节点w为BLACK,然后w的左孩子是红色,右孩子是黑色
在这里插入图片描述
此时将w染红,d染黑(可以理解为d,w先都染红,这时候旋转是不会引起黑高改变的,然后为了保持跟父子不能同时为红的性质,再将父节点染黑),再将w右旋,进入情况4

情况4. x的兄弟节点w为BLACK,然后w的右孩子是红色的
在这里插入图片描述
此时将 w.color = x.p.color, x.p.color = B, w.right.color = B,然后再进行一次左旋,此时我们会发现x的兄弟变为了e,而e的黑高为n-1,满足所有的红黑性质了。这时候跳出循环。

这里贴上RB-DELETE-FIXUP(T,x)的伪代码

def RB-DELETE-FIXUP(T,x):
	while x.color == B && x!=T.root:
		if x == x.p.left:
			w = x.p.right
			if w.color == R:
				w.color = B
				w.p.color = R
				LEFT-ROTATE(T, x.p)
				w = x.p.right
			if w.left == B && w.right == B:
				w.color = R
				x = x.p
			else
				if w.right.color == B:
					w.left.color = B
					w.color = R
					RIGHT-ROTATE(T,w)
					w = x.p.right
				w.color = w.p.color
				x.p.color = B
				w.right.color = B
				LEFT-ROTATE(T, x.p)
				x = T.root
		else 
			#x为右子树的情况
	x.color = B

总结

删除的部分实在是有点复杂,看了半天也只是明白了这样做能够成功,但是不知道其中内部的原理,就是为什么要这样做,这样做的内部规律是什么???就像以前玩魔方的时候记的公式一样,知道这种情况下要这么做,然后就回到下一个情况,但是其中的原因却知之甚少,等到后面一定要再次研究一下,明白其中的原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值