如何实现红黑树的删除?

939 阅读12分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

红黑树删除的代码实现

写在前面

文章摘要

  1. 删除的节点在右边的情况
    • 几种简单的情况
    • 兄弟节点是红色
    • 兄弟节点是黑色
      • 兄弟节点至少有一个红色的子节点
      • 兄弟节点没有红色的子节点
  2. 删除的节点在左边的情况(与上面对称)
  3. 优化参数&完整代码

阅读准备

  • 建议阅读时间:10 ~ 15 分钟
  • 阅读前提:
  • 本篇文章着重点是红黑树删除的代码实现,可以配合一起看👆👆👆

二、如何用代码实现红黑树的删除

(0)实现前的准备

  • 分析了这么这么这么久,真是长叹息以掩涕兮了😨🥶😭

  • 只不过看看《红黑树的删除》中对删除的总结,其实也并不是学不会,对吧,如果还有些迷糊,再来看看代码是如何实现的,更迷糊一些吧~

  • 在开始实现:afterRemove()方法之前,我们先来做两件事情:

    • 1、在afterRemove()方法中添加一个参数,稍后再解释为什么

    image-20221114182620385

    • 2、复习二叉搜索树删除的代码逻辑

image-20221114181923856

(1)几种简单的情况

  • 先来看看这几种情况:
    • ①被删除的节点是红色
    • ②BST度为1节点
    • ③真正删除的是根节点
    • ④BST”度为2“的节点
  • 看我列了这么多,是不是有些... 别着急,你看看这几种情况的实现,就知道有多简单了

image-20221114183322790

  • 这几种情况可以说是最简单的情况了
    • ①:删除的是红色节点,不解释了
    • ②:如果传进来用于取代的子节点是红色,说明该节点肯定是度为 1 的节点嘛,看看删除的代码即可知道
    • ③:取出父节点,若发现父节点是空的。说明只有一个根节点,你把唯一的节点都删除了,直接返回就行了呗(当然,之后可能有其他情况,可能实际不只有一个节点,但是从逻辑上是,也能够满足)
    • ④:???都没看到这种情况啊?确实是,代码都不用写,因为真正被删除的节点在B树的最后一层,度又为2,就只能有一种情况了,再看看上面总结~

(2)下面讨论的情况都是删除的是BST中度为0的节点

  • 经过了上面的四种情况,能来到这里说明:
    • 在二叉搜索树中:删除的是度为0的节点
    • 在等价的4阶B树中:删除后会产生下溢现象
    • 在红黑树中:删除的是黑色的节点,且它自己一个子节点都没有
  • 通过上面的分析和总结,来到这里我们需要取出兄弟节点来讨论
  • 如何取出兄弟节点呢?和之前一样node.sibling()吗?
  • 其实不然,我们来看看,传入afterRemove()方法中节点的大致内存细节

image-20221116091051015

  • 可以发现,父节点对自己的引用都已经断掉了,只有自己内部还引用着父节点
  • 那我们来看看node.sibling()方法,还能拿到真正的兄弟节点吗?

image-20221116091847002

  • 看了取兄弟节点的逻辑,是不是发现,根本拿不到兄弟节点了。那该如何拿呢?
  • 来到这里删除的肯定是BST中度为0的叶子节点,而删除叶子节点,势必会清除父节点对自己的引用
  • 对父节点来说:要么是 parent.left会清空,要么是parent.right会清空,反过来想想
  • 既然删除的节点在父节点的哪一边,那边就会被清空。那我们就可以根据哪边是空的,逆推出删除的是左边,还是右边。如果是parent.left == null,那么兄弟节点就是:parent.right。反之亦然

image-20221116093437030

  • 兄弟节点找到了,直接开始判断吗?
  • 回看上面的分析,我们讨论的时候,都是用被删除的节点位于右子树来举例的。其中也提到了,若删除的节点在左边,对于左右操作而言,是完全对称的。并且如何染色也是一致的
  • 由此,就可以只实现一边,我们以上面的例子,位于右子树为例。另外一边只需要修改写好后的左右即可

image-20221116094531510

① BST兄弟节点是红色

image-20221112201409280

  • 也就是我们所说的,关系有些复杂的一种情况,我们需要将其转换成熟悉的情况,之后再统一处理

image-20221116150141290

  • 我们无非是想要将兄弟的子节点(侄子),变成待删除节点的兄弟,所以这里需要通过右旋,就交换了节点
  • 但是还需要维护红黑树的性质,所以将其原先的兄弟节点染成【黑色】,父节点染成【红色】
  • 最后别忘了要将新的兄弟节点:sibling 赋值

② BST兄弟节点是黑色

  • 通过上面的操作,成功将兄弟节点是红色的情况转换成了黑色,下面即可统一处理
  • 也就是说,来到这里,兄弟节点肯定是黑色,那我们就可以根据兄弟是否有红色的子节点来讨论了

image-20221116150828962

1、至少有一个红色的子节点

image-20221112091803449

  • 这种情况,兄弟节点很富裕,至少可以借一个给我,那就借一个给下溢的兄弟呗
  • 通过旋转来借节点的时候,会有三种情况:左左、左右、左左或左右,而且旋转完成后,都需要染色来维护性质,那我们可以先处理一些特殊情况:左右,之后就可以统一看成左左的情况了

image-20221116151424606

  • 经过上面的处理后:

image-20221116151934447

  • LR的情况变成了LL,而有两个红色孩子的情况左左或右右,本身就可以看做是LL
  • 所以能来到下面的分析,肯定都可以看成LL的情况

image-20221116153317504

  • 也就是需要旋转 + 染色
    • 因为是LL,所以将父节点右旋即可
    • 旋转后兄弟节点变成了新的中心节点,将中心节点继承旧父节点的颜色,再将中心节点的左右两边都染成黑色即可
  • 至于为什么要继承旧父节点的颜色,在上篇文章已经分析过了,可以结合前面的内容,分析分析哟~
2、一个红色的子节点也没有

image-20221112153839462

  • 最后一种情况,兄弟也借不了,只能向父节点求助了,也就是通过染色将父节点和兄弟节点合并

image-20221117183049570

  • 实际上是通过染色解决的下溢,这里就不多解释了
  • 还有一点值得注意的是:染色前,需要记录父节点的颜色
    • 如果原先是红色,说明父节点向下合并后,父节点并不会产生下溢现象,不需要额外处理
    • 但是如果原先是黑色,本身就只有一个黑节点了,向下合并后,父节点也将会出现下溢
  • 这里的处理逻辑,就是将父节点当做被删除的节点,递归执行删除后的逻辑
  • 所以,当我们将父节点传入afterRemove()方法之后,其中有一处需要调整:

image-20221117185652319

  • 因为我们仅仅是将父节点当做被删除的节点,所以它执行代码前,左右子节点的引用肯定不会发生变化,所以查看是否是左子节点,就得用以前的方式

(3)对称情况的处理(被删除的节点在左边)

  • 在上面,我们已经分析且实现了被删除节点在右边的所有情况。真的谢了✌️✌️
  • 一直说它和在左边是对称的。具体有多对称呢?

image-20221117191313334

  • 看我标红色的地方,就是交换左右即可
    • 左边取左,右边就变成取右
    • 左边右旋,右边就左旋
  • 当然,有些地方交换了也是一样的效果,就没有交换

(4)优化afterRemove()的参数

  • 看完了上面的实现,真的可以说是苦尽甘来啊!!!
  • 虽然实现了,我们之前留了一个问题:afterRemove()方法中添加了一个参数

image-20221117192740571

  • 而且这个参数,在整个实现逻辑中,仅在这里用到了

  • 还有在AVL树的实现中,其实也没有用到这个参数

  • 那我们能否做到,不要这个参数,统一参数呢?

  • 当初多传这个参数,是想在红黑树这里方便判断删除的是度为1的节点

  • 也就是需要找到用于取代删除节点的节点。其余的地方都不需要改变,仅仅需要处理上面代码的逻辑

  • 处理前,我们先看看,在二叉搜索树中,应该如何传入参数,这是一个值得思考的问题

  • 先来看看度不是0的情况,改回之前的实现即可【箭头指向的是修改后的代码】

image-20221117200717357

  • 唯一要修改的就是下面度为1的节点

image-20221117200441200

  • 因为AVL树和红黑树共用了一个删除模板。所以在考虑传入红黑树的参数时,还得保证AVL树删除后的处理是正常的
  • 之前在实现AVL树的时候,这里是将被删除节点node传入了afterRemove()方法
  • 而现在传入的是用于取代的child,那么我们先看看AVL树的实现

image-20221117201420172

  • 可以发现,其实并没有影响,这里需要拿到被删除节点的父节点就可以。而我们在传入child的时候,本身就已经将被删除节点的父节点赋值给child.parent了,逻辑也不需要修改

  • 所以并不会产生影响,那红黑树中呢?

  • 我们先来修改,再解释:

image-20221117195748292

  • 修改这里,应该很好理解,因为传进来的就只有一个参数了
  • 就是将上面分析的①②种情况,合并成一致的逻辑了。看看注释的内容

(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); // 说明向下合并后,它也会下溢,将它当做被删除的节点
            }
        }
    }
}
  • 至此,终于将红黑树的删除分析完成了
  • 如果你真的耐心的看到了这里,那和我一起击个掌怎么样~ 🤚🤚🤚

写在后面

本篇收获

  • 加强红黑树删除的分析
  • 能够用代码实现红黑树的删除逻辑
  • 复习树的旋转、B树的性质、红黑树的性质