红黑树学习笔记

辅助方法

由于红黑树的结点有颜色,所以要有一些方法来操作颜色,并且红黑树要用到兄弟结点,所以把获取某个结点的兄弟结点也抽象成一个方法,方便写代码的时候专注于红黑树的实现逻辑

辅助方法的代码

package com.plasticine.tree;

import java.util.Comparator;

public class RBTree<E> extends BBST<E> {

    private static final boolean RED = false;
    private static final boolean BLACK = true;

    public RBTree() {
        this(null);
    }

    public RBTree(Comparator<E> comparator) {
        super(comparator);
    }

    // ========================================= 辅助方法 =========================================

    /**
     * 给红黑树的结点上色
     *
     * @param node  普通的二叉树结点
     * @param color 颜色
     */
    private Node<E> color(Node<E> node, boolean color) {
        if (node == null) return null;

        ((RBNode<E>) node).color = color;

        return node;
    }

    /**
     * 把红黑树的结点染成红色
     *
     * @param node 普通结点
     */
    private Node<E> red(Node<E> node) {
        return this.color(node, RED);
    }

    /**
     * 把红黑树的结点染成黑色
     *
     * @param node 普通结点
     */
    private Node<E> black(Node<E> node) {
        return this.color(node, BLACK);
    }

    /**
     * 返回一个结点的颜色
     *
     * @param node 目标结点
     * @return 目标结点的颜色
     */
    private boolean colorOf(Node<E> node) {
        return ((RBNode<E>) node).color;
    }

    /**
     * 判断一个结点是否是红色的
     *
     * @param node 目标结点
     * @return 是否是红色
     */
    private boolean isRED(Node<E> node) {
        return this.colorOf(node) == RED;
    }

    /**
     * 判断一个结点是否是黑色的
     *
     * @param node 目标结点
     * @return 是否是黑色
     */
    private boolean isBLACK(Node<E> node) {
        return this.colorOf(node) == BLACK;
    }

    /**
     * 获取某个节点的兄弟结点
     *
     * @param node 目标结点
     * @return 目标结点的兄弟结点
     */
    private Node<E> sibling(Node<E> node) {
        if (node.isLeftChild()) return node.parent.right;
        if (node.isRightChild()) return node.parent.left;

        return null;
    }

    // ========================================= 辅助方法 =========================================

    /**
     * 红黑树的结点
     */
    private static class RBNode<E> extends Node<E> {

        boolean color = RED;

        public RBNode(E element, Node<E> parent) {
            super(element, parent);
        }
    }
}

添加结点

情况分析

  • 有12种可能,如下图所示
添加结点的12种可能

由于红黑树可以类比成4阶B树,所以都可以把红色结点提上来,和黑色结点组成一个B树的结点,那么添加结点的时候就只有以上的12种情况,分别是17、33、50、72、88的左右均可添加,以及46的左、76的右这两种情况

  • 按照父结点的颜色划分,可以分为两种情况

    1. 父结点是黑色结点 -- 有四种可能

      父结点是红色结点

      像这种情况,直接把结点插进去即可,afterAdd不需要做任何操作

    2. 父结点是红色结点

      父结点是黑色结点

      而像这种情况,就又可以分为两种情况考虑,区分的标准是叔父节点(即父结点的兄弟结点)的颜色

      1. 叔父节点是红色

        如图中的10、20、30、36这四种情况,叔父节点都是红色的,这个时候只需要进行两步操作

        1. 把父结点和叔父节点染成黑色
        2. 把祖父节点染成红色,并提到祖父节点的父结点那一层(也就是发生了上溢),这里提上去的过程可以看成把25插入到25的上一层中这种情况,即递归调用afterAdd,因为再往上一层插无非也就是那12种情况中的一种罢了,不会有别的可能
      2. 叔父节点是黑色(虚拟想象出的null结点视为黑色)

        如图中的48、52、60、74这四种情况

        • 以祖父结点出发,根据当前插入结点相对于祖父节点的位置来区分,这点和AVL树的左旋转右旋转情况一致,分为LL、LR、RL、RR四种

          LL --> 60
          LR --> 74
          RL --> 48
          RR --> 52
          

继承关系重整

  • 由于AVL树和红黑树都用到了旋转的操作,并且他们都有共同的特性 -- 平衡,所以可以抽象成一个平衡二叉搜索树这个类,记为BBST(Balanced Binary Search Tree)

    BBST代码

    package com.plasticine.tree;
    
    import java.util.Comparator;
    
    public class BBST<E> extends BinarySearchTree<E> {
    
        public BBST() {
            this(null);
        }
    
        public BBST(Comparator<E> comparator) {
            super(comparator);
        }
    
        /**
         * 左旋转
         *
         * @param grand 左旋转操作的 grand 结点
         */
        protected void rotateLeft(Node<E> grand) {
            Node<E> parent = grand.right;
            Node<E> child = parent.left;
    
            // 左旋转
            grand.right = child;
            parent.left = grand;
    
            afterRotate(grand, parent, child);
        }
    
        /**
         * 右旋转
         *
         * @param grand 右旋转操作的 grand 结点
         */
        protected void rotateRight(Node<E> grand) {
            Node<E> parent = grand.left;
            Node<E> child = parent.right;
    
            // 右旋转
            grand.left = child;
            parent.right = grand;
    
            afterRotate(grand, parent, child);
        }
    
        /**
         * 旋转后的共同操作 --> 无论左旋转还是右旋转都是一样的
         */
        protected void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) {
            // 更新整颗子树的根节点的 left/right 指向
            if (grand.isLeftChild()) {
                grand.parent.left = parent;
            } else if (grand.isRightChild()) {
                grand.parent.right = parent;
            } else {
                root = parent;
            }
    
            // 更新 grand parent child 的父结点
            // parent
            parent.parent = grand.parent;
            // child
            if (child != null) child.parent = grand;
            // grand
            grand.parent = parent;
        }
    
        /**
         * 统一旋转操作
         */
        protected void rotate(Node<E> r,
                              Node<E> b, Node<E> c,
                              Node<E> d,
                              Node<E> e, Node<E> f) {
    
            // 构造 a b c
            b.right = c;
            if (c != null) c.parent = b;
    
            // 构造 e f g
            f.left = e;
            if (e != null) e.parent = f;
    
            // 构造 b d f
            if (r.isLeftChild()) r.parent.left = d;
            else if (r.isRightChild()) r.parent.right = d;
            else root = d;
    
            d.left = b;
            d.right = f;
            d.parent = r.parent;
            b.parent = d;
            f.parent = d;
        }
    
    }
    
    • AVL树继承自BBST后要做出一点修改,因为原本的旋转代码中,涉及到更新高度的部分,而高度是AVL树特有的,红黑树并没有,所以抽象之后要把更新高度的代码去掉,让AVL树重写旋转的代码,用super调用完父类的方法后,加上更新高度的部分即可

    AVL树重写的部分

    /**
      * AVL 树在旋转后要进行更新高度的操作
    */
    @Override
    protected void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) {
        super.afterRotate(grand, parent, child);
        // 更新高度 --> 先后更新 grand parent
        updateHeight(grand);
        updateHeight(parent);
    }
    
    /**
      * AVL 树统一旋转操作之后要更新高度
    */
    @Override
    protected void rotate(Node<E> r, Node<E> b, Node<E> c, Node<E> d, Node<E> e, Node<E> f) {
        super.rotate(r, b, c, d, e, f);
        updateHeight(b);
        updateHeight(f);
        updateHeight(d);
    }
    

添加结点代码实现

/**
 * 红黑树添加结点后的处理 -- 用于维护红黑树保持红黑树的 5 条性质
 *
 * @param node 新添加的结点
 */
@Override
protected void afterAdd(Node<E> node) {
    Node<E> parent = node.parent;

    // 添加的是根节点 -- 根节点需要染成黑色
    if (parent == null) {
        black(node);
        return;
    }

    // 父结点是黑色结点 -- 有四种可能
    if (isBLACK(parent)) return;   // 不需要做任何操作,直接插入即可

    // 父结点是红色结点
    Node<E> uncle = sibling(parent);    // 叔父节点
    Node<E> grand = parent.parent;      // 祖父节点

    // 1. 叔父节点是红色 --> 发生上溢
    if (isRED(uncle)) {
        // 把父结点和叔父节点染成黑色
        black(parent);
        black(uncle);
        // 祖父节点染成红色
        red(grand);
        // 祖父节点插入到上一层
        afterAdd(grand);

        return;
    }

    // 2. 叔父节点是黑色 --> 旋转,需要先判断是哪种旋转情况
    if (parent.isLeftChild()) {     // L
        if (node.isLeftChild()) {   // LL
            black(parent);
        } else {    // LR
            rotateLeft(parent);
            black(node);
        }
        rotateRight(grand);
    } else {    // R
        if (node.isLeftChild()) {   // RL
            rotateRight(parent);
            black(node);
        } else {    // RR
            black(parent);
        }
        rotateLeft(grand);
    }
}

删除结点

删除结点时的情况分析

  • 由于是在二叉搜索树删除某个结点后才进行红黑树的相应调整,而二叉搜索树删除结点的时候,只会删除度为1或0的结点,如果度为2,最终删除的实际上还是度为1或0的结点,又由于红黑树等价于4阶B树,所以红黑树删除结点时遇到的情况也就三种,如下图所示

    红黑树删除结点的三种可能

删除红色结点

  • 直接删除即可,不会影响红黑树的五条性质

删除有一个红色子结点的黑色结点

  • 将该红色子结点染成黑色,作为新的B树结点即可

  • 这里就需要对afterRemove()进行重构了,因为这种情况删除时要用到用于替换被删除结点的红色结点,所以要给该方法传入用于替换的那个结点,也就是图中删除46时,50用来替换它,那么需要把50染成黑色,而原本的afterRemove()方法只传入了46,并没有传入50,因此没办法对50进行操作

    /**
     * 删除结点后,为了维护红黑树的 5 条性质而作出的额外操作
     *
     * @param node        被删除的结点
     * @param replacement 用于替换被删除结点的结点
     */
    @Override
    protected void afterRemove(Node<E> node, Node<E> replacement) {
    
        // 删除的是红色结点 --> 直接删除即可,不用做额外的处理
        if (isRED(node)) return;
    
        // 能来到这说明被删除的结点是黑色的,那么就要分该黑色结点是否有子结点
        if (isRED(replacement)) {
            // 把用于替换的结点染成黑色即可
            black(replacement);
            return;
        }
    }
    

删除黑色叶子结点

删除黑色叶子结点

分两种可能

  1. 被删除结点的sibling是黑色的,如删除图中的88这个结点,它的sibling是76,为黑色

    • sibling有多余结点可以借出来

    这种情况好处理,由于删除88后,88所在的B树结点发生了下溢(因为4阶B树非根节点里的结点个数是1~3个,而删除88后变为了0个),而sibling结点76这一个B树结点有两个结点,可以“借”一个给88这个B树结点,所以通过旋转即可完成

    1. 图中这种情况属于LR,对76左旋转后再对80右旋转
    2. 旋转后的中心节点变为了78,78要继承自原本中心节点的颜色,原本的中心节点是80,所以旋转后78仍然为红色
    3. 为了保持红黑树的性质,还需要在旋转后把左右B树结点的中心节点染成黑色
    调整后的结果

    还有其他情况,如LL、RR、RL等,只是在旋转操作时有区别,整体的操作还是一致的,就不解释了

    • sibling没有多余结点可以借出来

      父结点是红色

      父结点是红色

      比如这种情况下,删除88结点,它的兄弟结点是76,没有多余结点可以借出来,又因为88发生下溢,所以需要把父结点,也就是80拉下来和sibling合并,也就是80(父结点)变为黑色,76(兄弟结点)变为红色即可,处理后如下图所示

      处理后的结果

      父结点是黑色

      [图片上传失败...(image-e624d7-1630068498177)]

      删除88,把父结点80拉下来后,76要变成红色结点,如下图所示

      处理后的结果

      这时原本80位置的B树结点就发生了下溢,没关系,只用递归地去调用一次afterRemove(),把80作为被删除结点看待,传入即可

  2. 被删除结点的sibling是红色的,如下图所示

    sibling是红色
  • 删除88这个结点,从红黑树的角度看,它的sibling是红色结点55,而从B树的角度来看,88的sibling是76

  • 如果是在B树的角度看的话,直接套用前面的sibling是黑色结点的情况即可结局

  • 所以现在关键就是要在红黑树的角度来看,让76成为88的sibling,也就是要实现“我的侄子变成我的兄弟”这种骚操作,那么怎么进行这种B树父结点转成红黑树兄弟节点的操作呢?

    • 从被删除结点88的父结点出发,往左再往左,也就是LL的情况,对80进行右旋转后就可以让76变为80的子结点,这样88和76就是兄弟结点的关系了,如下图所示

      我的侄子变成我的兄弟
    • 这时候就回到了前面的兄弟结点是黑色的情况了,直接复用那部分的代码即可,不用写新的代码

代码

/**
     * 删除结点后,为了维护红黑树的 5 条性质而作出的额外操作
     *
     * @param node        被删除的结点
     * @param replacement 用于替换被删除结点的结点
     */
    @Override
    protected void afterRemove(Node<E> node, Node<E> replacement) {

        // 删除的是红色结点 --> 直接删除即可,不用做额外的处理
        if (isRED(node)) return;

        // 能来到这说明被删除的结点是黑色的,那么就要分该黑色结点是否有子结点
        // 有一个红色子结点
        if (isRED(replacement)) {
            // 把用于替换的结点染成黑色即可
            black(replacement);
            return;
        }

        Node<E> parent = node.parent;
        if (parent == null) return;     // 删除的是根节点
        // 来到这说明删除的是黑色叶子结点 --> 要根据 sibling 的颜色来处理
        // 获取 sibling

        /*
            父结点的左边是空的说明被删除结点 node 是左子节点
            父结点的左边非空,那可能是下溢传递时的递归调用的情况,这时候直接判断传进来的结点是左还是右即可
         */
        boolean isLeft = parent.left == null || node.isLeftChild();
        Node<E> sibling = isLeft ? parent.right : parent.left;

        // 左和右的情况是对称的
        if (isLeft) {   // 被删除结点在左边,兄弟结点在右边
            if (isRED(sibling)) {   // 兄弟结点是红色 --> 把兄弟结点转成黑色的情况,再由后面统一处理
                black(sibling);
                red(parent);
                rotateLeft(parent);
                // 旋转之后兄弟结点发生变化,需要更新
                sibling = parent.right;
            }

            // 能来到这说明兄弟结点一定是黑色的,即便是红色也已经被转成黑色的情况了
            // 判断兄弟结点有无多余结点可以借给我
            if (isBLACK(sibling.left) && isBLACK(sibling.right)) {  // 兄弟结点没有多余结点
                boolean parentIsBlack = isBLACK(parent);
                // 将父结点和兄弟结点合并解决下溢
                black(parent);
                red(sibling);
                if (parentIsBlack) {    // 父结点和兄弟结点合并后仍会发生下溢
                    afterRemove(parent, null);
                }
            } else {    // 兄弟结点至少有一个红色子结点
                // 判断兄弟结点左子节点是否是黑色,是的话则先要让兄弟结点左旋转
                if (isBLACK(sibling.right)) {
                    rotateRight(sibling);
                    sibling = parent.right;  // 左旋转之后 sibling 引用要更新
                }
                // 统一处理父结点右旋转
                // 把旋转后的中心节点染成和父结点一样的颜色
                color(sibling, colorOf(parent));
                black(parent);
                black(sibling.right);
                rotateRight(parent);
            }
        } else {    // 被删除结点在右边,兄弟结点在左边
            if (isRED(sibling)) {   // 兄弟结点是红色 --> 把兄弟结点转成黑色的情况,再由后面统一处理
                black(sibling);
                red(parent);
                rotateRight(parent);
                // 旋转之后兄弟结点发生变化,需要更新
                sibling = parent.left;
            }

            // 能来到这说明兄弟结点一定是黑色的,即便是红色也已经被转成黑色的情况了
            // 判断兄弟结点有无多余结点可以借给我
            if (isBLACK(sibling.left) && isBLACK(sibling.right)) {  // 兄弟结点没有多余结点
                boolean parentIsBlack = isBLACK(parent);
                // 将父结点和兄弟结点合并解决下溢
                black(parent);
                red(sibling);
                if (parentIsBlack) {    // 父结点和兄弟结点合并后仍会发生下溢
                    afterRemove(parent, null);
                }
            } else {    // 兄弟结点至少有一个红色子结点
                // 判断兄弟结点左子节点是否是黑色,是的话则先要让兄弟结点左旋转
                if (isBLACK(sibling.left)) {
                    rotateLeft(sibling);
                    sibling = parent.left;  // 左旋转之后 sibling 引用要更新
                }
                // 统一处理父结点右旋转
                // 把旋转后的中心节点染成和父结点一样的颜色
                color(sibling, colorOf(parent));
                black(parent);
                black(sibling.left);
                rotateRight(parent);
            }
        }
    }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容