红黑树学习笔记02

学习视频链接:红黑树学习
注:本篇文档仅是个人学习的简单记录(二刷上述链接的视频)
本篇内容承接的记录:红黑树学习笔记01

2023.4.10述:学得不行,建议重学

后续直接从B树的角度再学,觉得2-3-4树没学好

1.上一篇文档说节点是双向的,按照节点定义的意思,应该是每个节点都会有三个联系的节点,每断开一个节点就意味着有两个节点的断口需要重新设置,其实只要记得每个节点需要有三个节点关系就行,不需要什么双向什么的。

在这里插入图片描述

2.左旋程序再探

在这里插入图片描述
就上图而言,蓝色节点有两个节点关系(父和右子)需要更新,灰色节点有两个节点关系(父和左子)需要更新(如果有黄色节点的话,黄色节点有一个节点关系(父)要更新

private void leftRotate(RBNode p){
	if(p!=null){
		RBNode r = p.right;//先取出待旋转节点的右子节点
		
		//步骤一:处理待旋转节点的右子节点部分
		p.right = r.left;//将p(旧位置)右节点的左节点赋给p(新位置)的右节点,此时更新的是蓝色的右子
		if(r.left!=null){//说明上一步实现的结果是待旋转节点的右子节点不为空
			r.left.parent = p;//将右子节点的父节点指向带旋转节点,此时更新的是黄色的父,黄色节点完成所有关系更新
		}

		//步骤二:处理新节点和待旋转节点的父节点部分
		r.parent = p.parent;//将r的父节点指向p的父节点,此时更新的是灰色的父
		//注意p有两种情况:
		if(p.parent = null) {//表明p是根节点
			root = r;
		}
		//如果上述不满足,表明p不是根节点,那么就要判断p相对于其父节点是左节点还是右节点
		else if(p == p.parent.left){
			p.parent.left = r;//注意这是一个由上指下的指针
		}
		else {
			p.parent.right = r;//注意这是一个由上指下的指针
		}
		
		//步骤三:处理新节点的左子节点部分
		r.left = p;//此时更新的是灰色的左子,灰色节点完成所有关系更新
		p.parent = r;//此时更新的是蓝色的父,蓝色节点完成所有关系更新
		//至此所有关系完成更新
	}
}

3.调整程序再探

插入调整的情况只有3节点和4节点的插入需要调整,并且需要记住2-3-4树对应红黑树的情况

  1. 插入节点一定是红色的,如果插入的是根节点的位置还需变为黑色;
  2. 2节点对应到红黑树一定是黑色节点
  3. 3节点对用到红黑树上一定是上黑下红;
  4. 裂变状态(4节点的插入)对应到红黑树的变色是上红下黑
    (先是红黑树的最终状态是由2-3-4树得到的,然后再得到从初始状态到最终状态中间的变换规则)

上面那4个或许描述的情形不是很详细,这里有个详细一点的规则:

  1. 2-3-4树:新增元素+2节点合并(节点中只有1个元素)=3节点(节点中有2个元素)
    红黑树:新增一个红色节点+黑色父亲节点=上黑下红( 2节点)------不需要调整
  2. 2-3-4树:新增元素+3节点合并(节点中有2个元素)=4节点(节点中有3个元素)
    这里有4种小情况(左3 ,右3,还有2个左中右不需要调整)
    红黑树:新增红色节点+上黑下红 = 排序后中间节点是黑色,两边节点都是红色( 3节点)
  3. 2-3-4树:新增一个元素+4节点合并=原来的4节点分裂,中间元素升级为父节点,新增元素与剩下的其中一个合并
    红黑树:新增红色节点+爷爷节点黑,父节点和叔叔节点都是红色=爷爷节点变红+父节点和叔叔节点变黑,如果爷爷是根节点,爷爷节点再转为黑
    在这里插入图片描述
    接下来看回程序:
    getColor的逻辑是如果节点为空,所获取的颜色就是黑色,这一点在后续的判断节点是否为空时会很重要
return p = null?BLACK : p,.color

下面的程序对照注释和程序下面的图的每种情况,就很容易理解了,看看就懂,用下面图的情况来说,就相当于要写的就只有图1和图2的情况,图3转图1处理,下面图4、5、6就是上面的对称,左右互换就行。

private void fixAfterPut(RBNode x){
	x.color = RED;//插入进来的节点一定是红色的
	//只有父节点是红色节点才需要调整,对应2-3-4树的插入3-节点的情况和插入4-节点的情况,具体是哪种里面还会判断
	while(x!=null && x!=root && x.parent.color==RED){
		//1.x的父节点是祖父节点的左孩子(左3)
		if(getParent(x) == getLeftSon(getParent(getParent(x))){
		    //这里包含图1、图2和图3的情况
		    //这里注意有一个技巧点,就是如果一个节点是空节点,就默认颜色是黑色,也就是说只要节点颜色为红色,就一定不为空
			RBNode y = getRightSon(getParent(getParent(x)));//这是叔叔节点
			if(getColor(y) == RED) {
				//这里是图2的情况
				//通过判断父节点的兄弟节点来区分是插入3-节点还是插入4-节点,因为3-节点是只有一个子节点的,另一边的空子节点颜色是黑色
				//如果进入了这个程序段,就说明是插入4-节点的情况,这种情况不需要进行旋转操作,只需要变色
				setColor(getParent(x),BLACK);
				setColor(y,BLACK);
				setColor(getParent(getParent(x)),RED);
		       //这里有递归意思:在完成上述操作后,所调整的只是一个局部的情况,而我们需要将整棵红黑树进行调整,因此将x指针指向祖父节点继续调整	
				x=getParent(getParent(x));//祖父节点继续往上调整,
				//因为插入4-节点的裂变状态,上升节点对应到红黑树一定是红色的,就有可能发生冲突;
				//而单边的情况涉及到旋转,会将父节点设置为黑色,就不存在双红连接的情况,就不需要往上调整
			} 
			else {
			//这里是图1和图3的情况,注意不要遗漏那个特殊的“左三”(图3)
				if(x==getRightSon(getParent(x))){
					//这里是图3的情况
					x=getParent(x);
					leftRotate(x);
					//这里就转为图1的情况了
				}
				//后续是图1需要的操作
				setColor(getParent(x),BLACK);//父节点变色
				setColor(getParent(getParent(x) ) , RED);//祖父节点变色
				rightRotate(getParent(getParent(x));//以祖父节点为对象进行右旋
				x = getParent(getParent(x));//继续往上调整
			}
		}

		//2.x的父节点是祖父节点的右孩子(右3)
		else {
			//这里包含图4、图5和图6的情况
		    //这里注意有一个技巧点,就是如果一个节点是空节点,就默认颜色是黑色,也就是说只要节点颜色为红色,就一定不为空
			RBNode y = getLeftSon(getParent(getParent(x)));//这是叔叔节点
			if(getColor(y) == RED) {
				//这里是图2的情况
				//通过判断父节点的兄弟节点来区分是插入3-节点还是插入4-节点,因为2-节点是只有一个子节点的,另一边的空子节点颜色是黑色
				//如果进入了这个程序段,就说明是插入4-节点的情况,这种情况不需要进行旋转操作,只需要变色
				setColor(getParent(x),BLACK);
				setColor(y,BLACK);
				setColor(getParent(getParent(x)),RED);
				x=getParent(getParent(x));//祖父节点继续往上调整
			} else {
			//这里是图4和图6的情况,注意不要遗漏那个特殊的“右三”(图6)
				if(x==getLeftSon(getParent(x))){
					//这里是图6的情况
					x=getParent(x);
					rightRotate(x);
				}
				//后续是图4需要的操作
				setColor(getParent(x),BLACK);//父节点变色
				setColor(getParent(getParent(x)),RED);//祖父节点变色
				leftRotate(getParent(getParent(x));//以祖父节点为对象进行右旋
			}
		}
	}
	//在完成上述调整后,根节点的颜色一定是红色,要转成黑色
	root.color = BLACK;
}

4.删除程序再探

删除的话分成两步:一个是找到待删除点以及它的替换点,用替换点的值换给待删除点,就相当于“删除”了待删除点,然后第二步就是删除替换点,这一步是真正的删除节点,最后调整。
第一步的程序对照程序的注释应该差不多了,主要还是第二步

//第一步:找前驱或者后继
private void predecessor(RBNode node){
	if (node==null){
		return null;
	}
	else if(node.left != null){
		RBNode p = node.left;
		while(p.right!=null){
			p=p.right;
		}
		return p;
	}
	else{//没有左子树的情况,沿着父节点往回找到根节点
		RBNode p = node.parent;
		RBNode ch = node;
		while(p!=null && ch !=p.right){
			ch = p;
			p= p.parent;
		}
		return p;
	}
}

//找后继节点
private void sucessor(RBNode node){
	if (node==null){
		return null;
	}
	else if(node.right != null){
		RBNode p = node.left;
		while(p.left!=null){
			p=p.left;
		}
		return p;
	}
	else{//没有左子树的情况,沿着父节点往回找到根节点
		RBNode p = node.parent;
		RBNode ch = node;
		while(p!=null && ch !=p.left){
			ch = p;
			p= p.parent;
		}
		return p;
	}
}
//第一步:替换节点的值赋给待删除节点
public V remove(K key){
	RBNode node=getNode(key);//先找到待删除节点
	if(node==null){
		return null;
	}
	V oldValue = node.value;
	deteleNode(node);
	return oldValue;
}

private RBNode getNode(K key){
	RBNode node = this.root;
	while(node!=null){
		if(key > node.key){
			node = node.right;
		}
		else if(key < node.key){
			node = node.left;
		}
		else{
			return node;
		}
	}
	return null;
}


//删除操作有三种情况:1.删除叶子结点,直接删除;2.删除的节点有一个子节点,用子节点替代;3.删除的节点有两个子节点,需要找到前驱或者后继来替代;
//注:这个删除节点函数不仅是用于删除叶子结点,应该是删除任意节点
private void deteleNode(RBNode node){
	//情况3.node节点有两个孩子
	if(node.left!=null && node.right!=null){
		RBNode successor = successor(node);//找到后继节点
		node.key = successor.key;//赋值
		node.value = successor.value;
		node = successor;//将node指针指向successor
		//到这一步就只会出现两种情况,要么找到的右子树的最左边是叶子节点,要么就是右子树的最左边节点后带有一个右子节点
	}
	//如果符合if条件,上面的if程序段执行完后,节点指针就会是图7,图8两种情况,是一定会来到有子树的最左边;
	//注意这里的情况还包括原来的待删除节点只有一棵子树的情况,也就是不走if的程序段,其实这里的情况就包括了图7、8,
	
	//三目运算符就表示如果有子节点就取出子节点,没有子节点就取出空节点,接下来就要区分是否为根节点
	RBNode replacement = node.left!=null?node.right:node.right;
	//替代节点不为空
	if(replacement != null){
		replacement.parent = node.parent;
		//注意在待删除节点不是叶子节点的情况下,又有两种情况了,一是待删除节点的父节点是根节点,二是正常情况
		if(node.parent = null){
		//说明待删除节点是根节点
			root = replacement;
		}
		else if(node == node.parent.left) {
		//待删除节点属于是一个左子节点的性质
			node.parent.left = replacement;
		}
		//如果上面两个if都不满足,说明待删除节点属于是一个右子节点的性质
		node.parent.right = replacement;
		//将待删除节点删除后,接下来就要考虑颜色调整的问题
		if(node.color == BLACK){
			//如果待删除节点的颜色是黑色就一定要调整,因为黑色节点被删除了,就会出现两个红色节点相邻的情况,红黑树的平衡就是黑色平衡
			//这种情况替代节点是红色的,此时只需要变色
			fixAfterRemove(replacement);
		}
	}
	
	//第二步:删除节点
	//接下来这一部分才涉及真正的删除操作,前面都是用其他节点替代待删除节点的值,然后将删除这个操作的对象转移到其他节点上
	//如果没有执行上面的if(replacement != null)里的程序,说明待删除节点是叶子结点,这时也有两种情况,要么待删除节点是根节点,要么不是根节点
	else if(node.parent == null){
		root = null;
	}
	else{
		//这时就是真正的叶节点删除
		//先调整颜色
		if(node.color == BLACK){
			//如果待删除节点的颜色是黑色就一定要调整,因为黑色节点被删除了,就会出现两个红色节点相邻的情况,红黑树的平衡就是黑色平衡
			fixAfterRemove(replacement);//就是变色和调位置
		}
		//再进行删除,红色节点直接删
		if(node.parent != null){
			if(node == node.parent.left){
				node.parent.left=null;
			}
			else if(node == node.parent.right){
				node.parent.right=null;
			}
			node.parent=null;
		}
	}
}

接下来就大头(待删除节点是黑色),
情况一是删除3节点和4节点,3节点的话就是将子节点替换上去的,然后颜色变黑就行,4节点的话也差不多
在这里插入图片描述
在删除2节点时,找的兄弟节点指的是在2-3-4树里同一行的兄弟节点,不是指红黑树的

private void fixAfterRemove(RBNode x){
	while(x!=root&&getColor(x)==BLACK){
		//x是左孩子的情况
		if(x==getLeftSon(getParent(x))){
			RBNode rnode = getRightSon(getParent(x))//注意这是红黑树的获取
			//判断此时兄弟节点是否是真正的兄弟节点(即是不是2-3-4树对应的那个兄弟),代码下面的文字有 代码解释----兄弟节点的情况:
			if(getColor(rnode)==RED){
				setColor(rnode,BLACK);
				setColoe(getParent(x),RED);
				leftRotate(getParent(x));
				rnode = getRightSon(getParent(x));//找到真正的兄弟节点
			}
			
			if(getColor(getLeftSon(rnode))==BLACK && getColor(getRightSon(rnode))==BLACK){
			//注意这里的代码判断的不是黑色,而是判断是否为空,因为getColor定义时是默认空节点为黑色
			//情况三,找兄弟借,兄弟没得借(兄弟没有子节点,2节点),一直往上递归调整黑色
				setColor(rnode,RED);
				x=getParent(x);
			}
			else{
				//情况二,找兄弟借,兄弟有得借(这里也分为两种情况,兄弟节点为3节点或者4节点)
				//还有rnode是一个指向节点的指针,左旋后rnode就指向了原来兄弟节点的子节点的位置,但我们需要的还是指向原来的兄弟节点的位置,这就是最后一句rnode=getRightSon(getParent(x));的意义
				if(getColor(getLeftSon(rnode))==BLACK){
					//右孩子为空,兄弟节点为三节点
					setColor(getLeftSon(rnode),BLACK);
					setColor(rnode,RED);
					rightRotate(rnode);
					rnode=getRightSon(getParent(x));
				}
				//兄弟节点为4节点,此时可以有两种做法,一是父亲下来,从兄弟节点借一个节点上去,这种情况需要旋转两次;二是父亲下来,从兄弟节点借两节点,一个上去,一个移到父亲旁边,这种情况只需要旋转一次,因此下面的代码是借两
				setColor(rnode,getColor(getParent(x));
				setColor(getParent(x),BLACK);
				setColor(getRightSon(rnode),BLACK);
				leftRotate(getParent(x));
				x=root;//结束这轮
			}
	}
	else{
	RBNode rnode = getLeftSon(getParent(x))//判断此时兄弟节点是否是真正的兄弟节点(即是不是2-3-4树对应的那个兄弟),代码下面的文字有 代码解释----兄弟节点的情况:
			if(getColor(rnode)==RED){
				setColor(rnode,BLACK);
				setColoe(getParent(x),RED);
				RightRotate(getParent(x));
				rnode = getLeftSon(getParent(x));//找到真正的兄弟节点
			}
			
			if(getColor(getRightSon(rnode))==BLACK && getColor(getLeftSon(rnode))==BLACK){
			//注意这里的代码判断的不是黑色,而是判断是否为空,因为getColor定义时是默认空节点为黑色
			//情况三,找兄弟借,兄弟没得借(兄弟没有子节点,2节点),一直往上递归调整黑色
				setColor(rnode,RED);
				x=getParent(x);
			}
			else{
				//情况二,找兄弟借,兄弟有得借(这里也分为两种情况,兄弟节点为3节点或者4节点)
				if(getColor(getRightSon(rnode))==BLACK){
					//右孩子为空,兄弟节点为三节点
					//这里就是先变色在调整位置,先将兄弟节点和兄弟节点的子节点变色换位,再整体换位将兄弟节点的子节点当父节点
					setColor(getRightSon(rnode),BLACK);
					setColor(rnode,RED);
					leftRotate(rnode);
					rnode=getLeftSon(getParent(x));
				}
				//兄弟节点为4节点,此时可以有两种做法,一是父亲下来,从兄弟节点借一个节点上去,这种情况需要旋转两次;二是父亲下来,从兄弟节点借两节点,一个上去,一个移到父亲旁边,这种情况只需要旋转一次,因此下面的代码是借两
				setColor(rnode,getColor(getParent(x));
				setColor(getParent(x),BLACK);
				setColor(getLeftSon(rnode),BLACK);
				rightRotate(getParent(x));
				x=root;//结束
			}
		}
	}
	//情况一:如果替代节点是红色节点,直接染黑
	setColor(x,BLACK);
}

情况二的一种变换图:
在这里插入图片描述

情况二的另一种变换图(有两种方法)
在这里插入图片描述
而右边旋转一次的方式效率更高,上面的结果对应的2-3-4树为(颜色根据2-3-4树的性质已经完成调整):所以我们需要做的就是用这样的方式来调整位置,在按照2-3-4数对应到红黑树的节点颜色规则调整颜色(2节点一定黑,4节点中间黑两边红)
在这里插入图片描述
情况三:
在这里插入图片描述
如果50是黑的就一直往上调整另一边的兄弟节点,然后这边往上,直到遇到红色节点就和上面一样

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜以冀北

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值