学习视频链接:红黑树学习
注:本篇文档仅是个人学习的简单记录(二刷上述链接的视频)
本篇内容承接的记录:红黑树学习笔记01
2023.4.10述:学得不行,建议重学
目录
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树对应红黑树的情况
- 插入节点一定是红色的,如果插入的是根节点的位置还需变为黑色;
- 2节点对应到红黑树一定是黑色节点
- 3节点对用到红黑树上一定是上黑下红;
- 裂变状态(4节点的插入)对应到红黑树的变色是上红下黑
(先是红黑树的最终状态是由2-3-4树得到的,然后再得到从初始状态到最终状态中间的变换规则)
上面那4个或许描述的情形不是很详细,这里有个详细一点的规则:
- 2-3-4树:新增元素+2节点合并(节点中只有1个元素)=3节点(节点中有2个元素)
红黑树:新增一个红色节点+黑色父亲节点=上黑下红( 2节点)------不需要调整 - 2-3-4树:新增元素+3节点合并(节点中有2个元素)=4节点(节点中有3个元素)
这里有4种小情况(左3 ,右3,还有2个左中右不需要调整)
红黑树:新增红色节点+上黑下红 = 排序后中间节点是黑色,两边节点都是红色( 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是黑的就一直往上调整另一边的兄弟节点,然后这边往上,直到遇到红色节点就和上面一样