《算法—深入浅出》红黑树的删除

一、《算法—深入浅出》N叉树的介绍
二、《算法—深入浅出》红黑树的旋转
三、《算法—深入浅出》红黑树的插入
四、《算法—深入浅出》红黑树的删除

一、前言

在《红黑树的旋转》一篇中提到过,RBT的删除稍微比插入要复杂一点,那么如何复杂?又是如何删除与调整?本篇将会给大家揭开面纱!

为了能更清楚简单的描述整个过程,本篇将分为:

  1. 查找要删除的节点;
  2. 查找可以替换删除的节点;
  3. 删除后不满足红黑树条件时的调整;

二、查找要删除的节点

给定一个节点key 或 value,根据红黑树的特点:

  • 所有左子树上的节点值一定小于根节点的值;
  • 所有右子树上的节点值一定大于根节点的值;
    那么,我们就可以通过二分法,来快速查找我们要删除的节点的 key 或 value。
public class RBTree {
    /*******************************************************************************************************************
     * 删除指定结点
     *******************************************************************************************************************/
    public void remove(int value) {
        if (root == null) {
            return;
        }
    
        /**************************************************************************************
         * 二分法,先查找需要删除的节点
         **************************************************************************************/
        TreeNode p = root;
        while (p != null) {
            if (p.value == value) {
                break;
            } else if (p.value > value) {
                p = p.left;
            } else {
                p = p.right;
            }
        }
    
        // 没有找到指定值的节点
        if (p == null) {
            return;
        }
    
        // 查找可以替换删除的节点
        ......
        
        // 调整红黑树
        ......
    }
}

三、查找可替换删除的节点

如下图,我们要删除值为5的节点:


image.png

如果我们直接删除节点5,那么就要选择节点2 或者 节点8为新的根节点(取代节点5这个根,不是整个 RBT 的根),无论选择哪个节点为新根节点,都会不满足红黑树的第5点,从 RBT 的根到任意叶子节点,其路径上的黑色节点数相同。
那么,我们就没有好的办法了么?
办法肯定是有的,而且还有两种办法,当然,实际上只是一种方法,只是采取的策略不同而已!
首先,我们发现,比5次小的数是节点2的右节点,而比5次大的数是节点8的左节点,因此,我们可以选举节点3或节点6来成为新的节点,我们看看分别选用新的节点后,红黑树的结果如何?


前驱or后继.png

我们发现,无论是采用『前驱选举法』还是『后继选举法』选举出的新根节点,来取代老节点,都满足红黑树的5条要求,而且不用调整(后文中会讲道哪些情况下不用调整,哪些情况下需要调整)。

那么前驱与后继是如何实现的,又有什么要求?

  1. 被删除的节点只要存在儿子节点:
    a. 如果左儿子存在,则先定位到其左儿子节点,然后不断的定位到右儿子节点,直到下一个右儿子节点为NULL;
    a. 同理,右儿子存在,则先定位到其右儿子节点,再不断定位左儿子节点,直到下一个左儿子节点为NULL;
    c. 对于 a 或者 b,只需要将最右或最左节点的值赋值给被删除的节点,然后将删除的指向指到最右或最左子节点;
    d. 对于最右或最左,可能也存在儿子,因此,继续执行第 1 点,\color{red}{最终会执行到 e,无儿子的节点}
    e. 如果最右或最左没有儿子,则执行第 2 点;
  2. 如果两个儿子都不存在,则查看下节内容;
  • 递归查找替换节点(后继):


    红黑树的删除递归.png

查找前驱或后继节点,并替换删除节点的值(若有两个儿子,先前驱还是先后继都可以)

public class RBTree {
    public void remove(int value) {
        // 二分法查找待删除的节点
        ......
        // 查找可以替换删除的节点
        p = findPreOrNextNode(p);
        // 调整红黑树
        ......
    }
    
    /*******************************************************************************************************************
     *  查找后继或前驱节点,最终转化为删除(含有一个 or 零个)的子节点
     * 【返回最终要删除的子节点】
     *******************************************************************************************************************/
    private TreeNode findPreOrNextNode(TreeNode p) {
        if (p.left != null || p.right != null) {
            // g 为指向删除的节点
            TreeNode g = p;
    
            /**********************************************************************************
             * 查找
             * 1. 后继:仅比待删除节点次大的节点
             * or
             * 2. 前驱:仅比待删除节点次小的节点
             **********************************************************************************/
            if (p.right != null) {
                // 查找后继节点
                p = p.right;
                while (p.left != null) {
                    p = p.left;
                }
            } else {
                // 查找前驱节点
                p = p.left;
                while (p.right != null) {
                    p = p.right;
                }
            }
    
            /**********************************************************************************
             * 交换【待删除节点】与【后继/前驱】的值,改为删除【后继/前驱】节点
             **********************************************************************************/
            g.value = p.value;
    
            p = findPreOrNextNode(p);
        } else {
            /**********************************************************************************
             * 待删除节点没有左、右儿子,就是删除自己
             **********************************************************************************/
        }
        return p;
    }
}

四、调整红黑树

如果两个儿子都不存在,则需要考虑被删除节点的颜色(\color{red}{以下考虑前提:待删除节点为左节点;}右节点同理,只是条件相左):

  1. 如果是红色,则直接删除,不用后续调整;
  2. 如果是黑色,则需要考虑其兄弟节点颜色,以及兄弟节点的儿子情况:
    \color{red}{a.} 如果兄弟节点是红色,则要满足红黑树第5点,兄弟节点必有两个黑色的儿子,则修改兄弟节点的左儿子为红色,兄弟节点为黑色,对父节点左旋,调整完毕;
    \color{red}{b.} 如果兄弟节点是黑色(如果有儿子,则一定是红色,黑色则不满足红黑树第5点):
    \color{red}{i.} 兄弟节点有一个右儿子:将父节点颜色给兄弟节点,修改父节点和兄弟右儿子节点为红色,对父节点左旋,调整完毕;
    \color{red}{ii.} 兄弟节点有一个左儿子,互换兄弟与其左儿子节点颜色,对兄弟节点右旋,此时和 2.b.i 一样,执行即可;
    \color{red}{ii.} 兄弟节点有两个儿子,无视兄弟左儿子节点,则该情况其实和 2.b.i 一样,执行 2.b.i 流程;
    \color{red}{iv.} 兄弟节点没有儿子,因为删除的节点为黑色,为了动态平衡,直接修改兄弟节点的颜色为红色,但此时可能不满足红黑树第4点(父节点可能是红色),因此,将待调整节点指为父节点,继续执行第 2 点;

先假设各节点的名称:

  • X为待删除节点;
  • P为父节点;
  • B为兄弟节点;
  • BL为兄弟节点的左节点;
  • BR为兄弟节点的右节点;

下面将对第 2 点中的 5 种情况用图来直观的分析:

  • 2.a:X = 黑色,B = 红色,BL = 黑色,BR = 黑色


    2.a.png
  • 2.b.i:X = 黑色,B = 黑色,BR = 红色

  • 2.b.ii:X = 黑色,B = 黑色,BL = 红色

  • 2.b.iii:X = 黑色,B = 黑色,BL = 红色,BR = 红色


    2.b.i-iii.png
  • 2.b.iv:X = 黑色,B = 黑色


    2.b.iv.png
  • 删除结点后的检查或调整:

public class RBTree {
    public void remove(int value) {
        // 二分法查找待删除的节点
        ......
        // 查找可以替换删除的节点
        ......
        // 调整红黑树,并将 X 节点移除
        fixedRemove(p);
        if (p == p.parent.left) {
            p.parent.left = null;
        } else {
            p.parent.right = null;
        }
    }
    
    /*******************************************************************************************************************
     * cur为新的删除节点,需要考虑:
     * 1、cur 没有儿子:
     *    1.1、cur 为红色节点,则直接删除;
     *    1.2、cur 为黑色节点,需要考虑其兄弟节点;
     * 2、cur 有一个儿子:
     *    2.1、交互 cur 与其儿子节点的值,改为删除儿子节点;
     *    2.2、重复第1步;
     * 3、cur 不可能有两个儿子:
     *    3.1、根据 remove,cur 要么为后继、要么为前驱、要么没有儿子;
     *    3.2、所以只存在上述1、2;
     *    3.3、且第2步最终也转化为第1步需要考虑的 1.1 或 1.2 情况;
     *******************************************************************************************************************/
    private void fixedRemove(TreeNode cur) {
        while (cur != root && cur.color == TreeNode.BLACK) {
            /***********************************************************************************************************
             * cur 为待删除节点
             * b 为兄弟节点
             * p 为父节点
             *
             * 【以下分析基于 cur 为左节点,若为右节点,则条件相反】
             * 因为 cur 节点存在且为黑色,所以,其兄弟节点一定存在:
             *
             * 1、若 b 节点为红色,则满足红黑树条件的情况下,b 的儿子节点一定存在,且有两个为黑色的儿子:
             *    设 b 左儿子为红色,b 为黑色,对 p 向左旋转;
             *
             * 2、若 b 节点为黑色:
             *    2.1、b 有一个右儿子(一定是红色),将 p 的颜色给 b,p 和 b 的右儿子设为黑色,对 p 向左旋转;
             *    2.2、b 有一个左儿子(一定是红色),将儿子设为黑色,b设为红色,b 是右节点,对 b 向右旋转;(此时情况同 2.1)
             *    2.3、b 有两个儿子(一定是红色),此时,处理同 2.1;
             *
             *    2.4、b 没有儿子,则设 b 为红色,cur 指向 p,继续向上递归至根节点,或遇到红色节点为止;
             *        之所以要向上递归,是因为 p 可红可黑,b 从黑色改变为红色,此时就少了一个黑色节点,条件5可能不满足,
             *        需要检查 p 节点颜色及其兄弟;
             ***********************************************************************************************************/
    
            TreeNode p = cur.parent;
            TreeNode b;
    
            if (cur == p.left) {
                /*********************************************************************************
                 * 待删除节点为左节点
                 *********************************************************************************/
                b = p.right;
    
                // 1
                if (b.color == TreeNode.RED) {
                    b.left.color = TreeNode.RED;
                    b.color = TreeNode.BLACK;
                    rotateLeft(p);
                    break;
                }
    
                // 2.4
                if (b.left == null && b.right == null) {
                    b.color = TreeNode.RED;
                    cur = p;
                    continue;
                }
    
                // 2.2
                if (b.left != null && b.right == null) {
                    b.left.color = TreeNode.BLACK;
                    b.color = TreeNode.RED;
                    rotateRight(b);
                }
    
                // 2.1、2.3 以及 2.2 -> 2.1
                b.color = p.color;
                p.color = b.right.color = TreeNode.BLACK;
                rotateLeft(p);
                break;
    
            } else {
                /******************************************************************************
                 * 待删除节点为右节点
                 ******************************************************************************/
                b = p.left;
    
                // 1
                if (b.color == TreeNode.RED) {
                    b.right.color = TreeNode.RED;
                    b.color = TreeNode.BLACK;
                    rotateLeft(p);
                    break;
                }
    
                // 2.4
                if (b.left == null && b.right == null) {
                    b.color = TreeNode.RED;
                    cur = p;
                    continue;
                }
    
                // 2.2
                if (b.right != null && b.left == null) {
                    b.right.color = TreeNode.BLACK;
                    b.color = TreeNode.RED;
                    rotateLeft(b);
                }
    
                // 2.1、2.3 以及 2.2 -> 2.1
                b.color = p.color;
                p.color = b.left.color = TreeNode.BLACK;
                rotateRight(p);
                break;
            }
        }
    
        // 可能是2.4退出,将 p 节点强制黑色,以实现动态平衡
        cur.color = TreeNode.BLACK;
    }
}

五、测试及结果

public class Main {

    public static void main(String[] args) {
        rbTree();
    }

    private static void rbTree() {
        int[] values = new int[]{
                12, 1, 9, 2, 0, 11, 7, 19, 4, 15, 18, 5, 14, 13, 10, 16, 6, 3, 8, 17
        };

        RBTree tree = new RBTree(null);

        // 保持输出与【https://www.cs.usfca.edu/~galles/visualization/RedBlack.html】一致的效果
        // 优先前驱
        tree.setLeftFirst(true);

        for (int value : values) {
            tree.insert(value);
        }

        tree.doPreTravel();
        tree.remove(14);
        tree.doPreTravel();
    }
}
  • 完全插入完的RBT


    完整的RBT.png
  • 删除节点14后的RBT


    删除节点14后的RBT.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容