开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情
红黑树删除的代码实现
写在前面
文章摘要
- 删除的节点在右边的情况
- 几种简单的情况
- 兄弟节点是红色
- 兄弟节点是黑色
- 兄弟节点至少有一个红色的子节点
- 兄弟节点没有红色的子节点
- 删除的节点在左边的情况(与上面对称)
- 优化参数&完整代码
阅读准备
- 建议阅读时间:10 ~ 15 分钟
- 阅读前提:
- 红黑树删除的分析 -> 红黑树删除的讨论分析♨️♨️♨️
- 本篇文章着重点是红黑树删除的代码实现,可以配合一起看👆👆👆
二、如何用代码实现红黑树的删除
(0)实现前的准备
-
分析了这么这么这么久,真是长叹息以掩涕兮了😨🥶😭
-
只不过看看《红黑树的删除》中对删除的总结,其实也并不是学不会,对吧,如果还有些迷糊,再来看看代码是如何实现的,更迷糊一些吧~
-
在开始实现:
afterRemove()方法
之前,我们先来做两件事情:- 1、在
afterRemove()方法中
添加一个参数,稍后再解释为什么
- 2、复习二叉搜索树删除的代码逻辑
- 1、在
(1)几种简单的情况
- 先来看看这几种情况:
- ①被删除的节点是红色
- ②BST度为1节点
- ③真正删除的是根节点
- ④BST”度为2“的节点
- 看我列了这么多,是不是有些... 别着急,你看看这几种情况的实现,就知道有多简单了
- 这几种情况可以说是最简单的情况了
- ①:删除的是红色节点,不解释了
- ②:如果传进来用于取代的子节点是红色,说明该节点肯定是度为 1 的节点嘛,看看删除的代码即可知道
- ③:取出父节点,若发现父节点是空的。说明只有一个根节点,你把唯一的节点都删除了,直接返回就行了呗(当然,之后可能有其他情况,可能实际不只有一个节点,但是从逻辑上是,也能够满足)
- ④:???都没看到这种情况啊?确实是,代码都不用写,因为真正被删除的节点在B树的最后一层,度又为2,就只能有一种情况了,再看看上面总结~
(2)下面讨论的情况都是删除的是BST中度为0的节点
- 经过了上面的四种情况,能来到这里说明:
- 在二叉搜索树中:删除的是度为0的节点
- 在等价的4阶B树中:删除后会产生下溢现象
- 在红黑树中:删除的是黑色的节点,且它自己一个子节点都没有
- 通过上面的分析和总结,来到这里我们需要取出兄弟节点来讨论
- 如何取出兄弟节点呢?和之前一样
node.sibling()
吗? - 其实不然,我们来看看,传入
afterRemove()方法
中节点的大致内存细节
- 可以发现,父节点对自己的引用都已经断掉了,只有自己内部还引用着父节点
- 那我们来看看
node.sibling()
方法,还能拿到真正的兄弟节点吗?
- 看了取兄弟节点的逻辑,是不是发现,根本拿不到兄弟节点了。那该如何拿呢?
- 来到这里删除的肯定是BST中度为0的叶子节点,而删除叶子节点,势必会清除父节点对自己的引用
- 对父节点来说:要么是
parent.left
会清空,要么是parent.right
会清空,反过来想想 - 既然删除的节点在父节点的哪一边,那边就会被清空。那我们就可以根据哪边是空的,逆推出删除的是左边,还是右边。如果是
parent.left == null
,那么兄弟节点就是:parent.right
。反之亦然
- 兄弟节点找到了,直接开始判断吗?
- 回看上面的分析,我们讨论的时候,都是用被删除的节点位于右子树来举例的。其中也提到了,若删除的节点在左边,对于左右操作而言,是完全对称的。并且如何染色也是一致的
- 由此,就可以只实现一边,我们以上面的例子,位于右子树为例。另外一边只需要修改写好后的左右即可
① BST兄弟节点是红色
- 也就是我们所说的,关系有些复杂的一种情况,我们需要将其转换成熟悉的情况,之后再统一处理
- 我们无非是想要将兄弟的子节点(侄子),变成待删除节点的兄弟,所以这里需要通过右旋,就交换了节点
- 但是还需要维护红黑树的性质,所以将其原先的兄弟节点染成【黑色】,父节点染成【红色】
- 最后别忘了要将新的兄弟节点:
sibling 赋值
② BST兄弟节点是黑色
- 通过上面的操作,成功将兄弟节点是红色的情况转换成了黑色,下面即可统一处理
- 也就是说,来到这里,兄弟节点肯定是黑色,那我们就可以根据兄弟是否有红色的子节点来讨论了
1、至少有一个红色的子节点
- 这种情况,兄弟节点很富裕,至少可以借一个给我,那就借一个给下溢的兄弟呗
- 通过旋转来借节点的时候,会有三种情况:
左左、左右、左左或左右
,而且旋转完成后,都需要染色来维护性质,那我们可以先处理一些特殊情况:左右
,之后就可以统一看成左左
的情况了
- 经过上面的处理后:
LR
的情况变成了LL
,而有两个红色孩子的情况左左或右右
,本身就可以看做是LL
- 所以能来到下面的分析,肯定都可以看成
LL
的情况
- 也就是需要
旋转 + 染色
- 因为是LL,所以将父节点右旋即可
- 旋转后兄弟节点变成了新的中心节点,将中心节点继承旧父节点的颜色,再将中心节点的左右两边都染成黑色即可
- 至于为什么要继承旧父节点的颜色,在上篇文章已经分析过了,可以结合前面的内容,分析分析哟~
2、一个红色的子节点也没有
- 最后一种情况,兄弟也借不了,只能向父节点求助了,也就是
通过染色将父节点和兄弟节点合并
- 实际上是通过染色解决的下溢,这里就不多解释了
- 还有一点值得注意的是:染色前,需要记录父节点的颜色
- 如果原先是红色,说明父节点向下合并后,父节点并不会产生下溢现象,不需要额外处理
- 但是如果原先是黑色,本身就只有一个黑节点了,向下合并后,父节点也将会出现下溢
- 这里的处理逻辑,就是将父节点当做被删除的节点,递归执行删除后的逻辑
- 所以,当我们将父节点传入
afterRemove()
方法之后,其中有一处需要调整:
- 因为我们仅仅是将父节点当做被删除的节点,所以它执行代码前,左右子节点的引用肯定不会发生变化,所以查看是否是左子节点,就得用以前的方式
(3)对称情况的处理(被删除的节点在左边)
- 在上面,我们已经分析且实现了被删除节点在右边的所有情况。真的谢了✌️✌️
- 一直说它和在左边是对称的。具体有多对称呢?
- 看我标红色的地方,就是交换左右即可
- 左边取左,右边就变成取右
- 左边右旋,右边就左旋
- 当然,有些地方交换了也是一样的效果,就没有交换
(4)优化afterRemove()
的参数
- 看完了上面的实现,真的可以说是苦尽甘来啊!!!
- 虽然实现了,我们之前留了一个问题:
afterRemove()方法
中添加了一个参数
-
而且这个参数,在整个实现逻辑中,仅在这里用到了
-
还有在AVL树的实现中,其实也没有用到这个参数
-
那我们能否做到,不要这个参数,统一参数呢?
-
当初多传这个参数,是想在红黑树这里方便判断删除的是度为1的节点
-
也就是需要找到用于取代删除节点的节点。其余的地方都不需要改变,仅仅需要处理上面代码的逻辑
-
处理前,我们先看看,在二叉搜索树中,应该如何传入参数,这是一个值得思考的问题
-
先来看看度不是0的情况,改回之前的实现即可【箭头指向的是修改后的代码】
- 唯一要修改的就是下面度为1的节点
- 因为
AVL树和红黑树
共用了一个删除模板。所以在考虑传入红黑树的参数时,还得保证AVL树
删除后的处理是正常的 - 之前在实现
AVL树
的时候,这里是将被删除节点node
传入了afterRemove()方法
- 而现在传入的是
用于取代的child
,那么我们先看看AVL
树的实现
-
可以发现,其实并没有影响,这里需要拿到被删除节点的父节点就可以。而我们在传入
child
的时候,本身就已经将被删除节点的父节点赋值给child.parent
了,逻辑也不需要修改 -
所以并不会产生影响,那红黑树中呢?
-
我们先来修改,再解释:
- 修改这里,应该很好理解,因为传进来的就只有一个参数了
- 就是将上面分析的①②种情况,合并成一致的逻辑了。看看注释的内容
(5)完整实现afterRemove(Node<E> node)
protected void afterRemove(Node<E> node) {
/*
①、被删除的节点是红色的情况
②、删除的节点有且仅有一个红色的子节点【也就是在二叉搜索树中删除的度为 1 的节点,找到取代它的子节点】
*/
if (isRed(node)) {
/*
①:如果被删除的节点是红色的情况,将它染黑也没关系,反正它的内存马上就要释放掉了
②:将用于代替被删除节点的子节点染成黑色即可
*/
black(node);
return;
}
Node<E> parent = node.parent; // 取出被删除节点的父节点
// ③、如果父节点是空的,说明删除的是根节点
if (parent == null) return;
/*
说明以前删除的是左边
1、(parent.left == null)是被删除的节点
2、(node.isLeftChild())是父节点下溢
*/
boolean isLeft = parent.left == null || node.isLeftChild();
// 左边为 null 说明右边是兄弟节点,否则是左边
Node<E> sibling = isLeft ? parent.right : parent.left;
if (isLeft) { // 被删除的节点在左边,与下面的操作对称
// 兄弟节点是红色
if (isRed(sibling)) { // 将其转换为兄弟节点是黑色的两种情况
rotateLeft(parent); // 左旋,变换兄弟节点
red(parent); // 父节点染成红色
black(sibling); // 兄弟节点染成黑色
sibling = parent.right; // 兄弟节点变了,改变引用
}
// 能来到这里,兄弟节点肯定是黑色的了
if (isRed(sibling.right) || isRed(sibling.left)) { // 兄弟至少有一个红色的子节点
if (isRed(sibling.left)) { // RL的情况,将其转换为RR,与下面统一处理
rotateRight(sibling); // 兄弟节点右旋
sibling = parent.right; // 兄弟节点变化了
}
// 能来到这里,说明都可以看成是RR的情况了
rotateLeft(parent); // 将父节点左旋
/*
1、兄弟节点变成了新的父节点(新的中心节点)
2、将新父节点的颜色继承旧父节点的颜色
3、将新父节点的左右子节点都染成黑色
*/
color(sibling, colorOf(parent));
black(sibling.left);
black(sibling.right);
} else { // 兄弟一个红色的子节点都没有
boolean isBlack = isBlack(parent); // 记录父节点原先的颜色
// 将父节点染成黑色,兄弟节点染成红色
black(parent);
red(sibling);
if (isBlack) { // 如果父节点原先就是黑色的
afterRemove(parent); // 说明向下合并后,它也会下溢,将它当做被删除的节点
}
}
} else { // 被删除的节点在右边,与上面的操作对称
// 兄弟节点是红色
if (isRed(sibling)) { // 将其转换为兄弟节点是黑色的两种情况
rotateRight(parent); // 右旋,变换兄弟节点
red(parent); // 父节点染成红色
black(sibling); // 兄弟节点染成黑色
sibling = parent.left; // 兄弟节点变了,改变引用
}
// 能来到这里,兄弟节点肯定是黑色的了
if (isRed(sibling.left) || isRed(sibling.right)) { // 兄弟至少有一个红色的子节点
if (isRed(sibling.right)) { // LR的情况,将其转换为LL,与下面统一处理
rotateLeft(sibling); // 兄弟节点左旋
sibling = parent.left; // 兄弟节点变化了
}
// 能来到这里,说明都可以看成是LL的情况了
rotateRight(parent); // 将父节点右旋
/*
1、兄弟节点变成了新的父节点(新的中心节点)
2、将新父节点的颜色继承旧父节点的颜色
3、将新父节点的左右子节点都染成黑色
*/
color(sibling, colorOf(parent));
black(sibling.left);
black(sibling.right);
} else { // 兄弟一个红色的子节点都没有
boolean isBlack = isBlack(parent); // 记录父节点原先的颜色
// 将父节点染成黑色,兄弟节点染成红色
black(parent);
red(sibling);
if (isBlack) { // 如果父节点原先就是黑色的
afterRemove(parent); // 说明向下合并后,它也会下溢,将它当做被删除的节点
}
}
}
}
- 至此,终于将红黑树的删除分析完成了
- 如果你真的耐心的看到了这里,那和我一起击个掌怎么样~ 🤚🤚🤚
写在后面
- ✌️✌️✌️完整代码
- 推荐阅读:
- 了解二叉搜索树 ->《二叉搜索树的实现与分析》
- 了解树的旋转 ->《透过AVL树的实现,学习树的旋转》
- 了解上溢现象 ->《你心里有B树吗?》
- 红黑树的添加 ->《红黑树添加的分析与实现》
本篇收获
- 加强红黑树删除的分析
- 能够用代码实现红黑树的删除逻辑
- 复习树的旋转、B树的性质、红黑树的性质