数据结构 --- 红黑树

本文详细介绍了红黑树的特性,包括其颜色规则和自平衡策略。在插入和删除操作中,通过旋转和颜色调整来保持树的平衡。此外,文章还提供了插入和删除节点的具体代码实现,并探讨了不同情况下如何维护红黑树的平衡状态。
摘要由CSDN通过智能技术生成

红黑树也是一种自平衡的二叉搜索树,和AVL树比较,插入和删除时,旋转的次数更少。

红黑树的特性:

  1. 所有节点都有颜色:红色或者黑色
  2. 所有null均视为黑色
  3. 红色节点不能相邻
  4. 根节点是黑色
  5. 从根节点到任意一个叶子节点,路径中的黑色节点数一样(黑色完美平衡)

构造红黑树节点 

  1. 初始颜色都为红色
  2. 带参数构造器,将key 和 value 传入
  3. 红黑树节点除了左右孩子节点外,还定义了父节点

    static class RBNode {
        int key;
        Object value;
        String color = "red";
        RBNode left;
        RBNode right;
        RBNode parent;

        public RBNode(int key, Object value) {
            this.key = key;
            this.value = value;
        }

        // 是否是左孩子
        boolean isLeftChild() {
            return parent != null && parent.left == this;
        }

        // 找到当前节点的叔叔节点
        RBNode uncle() {
            if (parent == null || parent.parent == null) {
                return null;
            }
            if (parent.isLeftChild()) {
                return parent.parent.right;
            } else {
                return parent.parent.left;
            }
        }

        // 找到当前节点的兄弟节点
        RBNode brother() {
            if (parent == null) {
                return null;
            }
            if (this.isLeftChild()) {
                return parent.right;
            } else {
                return parent.left;
            }
        }
    }

红黑树的左旋和右旋:

右旋,如图所示:

 

 代码:

画图,很容易就理解了,相比较AVL树,多了一个parent节点的赋值。

    // 这里重点介绍右旋,左旋也是类似的 
    public void rightRotate(RBNode pink) {
        // 红黑树要维护父亲节点,因此把涉及到的节点都先定义出来
        RBNode parent = pink.parent;
        RBNode yellow = pink.left;
        RBNode green = yellow.right;
        // 如果左子节点还存在右子节点,右旋后右子节点要挂载node节点的左节点位置 -- 维护pink和green的关系
        if (green != null) {
            green.parent = pink;
        }
        //  维护pink和green的关系,这里green是否null都不影响
        pink.left = green;

        // 左子节点和当前节点关系维护
        yellow.right = pink;
        pink.parent = yellow;

        // 维护和父节点的关系
        // 原父节点为null,说明是以根节点进行右旋,旋转完成之后的yellow也就是根节点了
        // 原父节点不为null,将yellow放在原来node节点的位置
        yellow.parent = parent;    
        if (parent == null) {
            root = yellow;
        } else if (parent.left == pink) {
            parent.left = yellow;
        } else {
            parent.right = yellow;
        }
    }

    public void leftRotate(RBNode pink) {
        RBNode parent = pink.parent;
        RBNode yellow = pink.right;
        RBNode green = yellow.left;

        if (green != null) {
            green.parent = pink;
        }
        yellow.left = pink;
        yellow.parent = parent;
        pink.right = green;
        pink.parent = yellow;

        if (parent == null) {
            root = yellow;
        } else if (parent.left == pink) {
            parent.left = yellow;
        } else {
            parent.right = yellow;
        }
    }

红黑树的插入操作

红黑树的插入操作,也是遵循平衡二叉树的插入操作的。不同的是,需要维护父亲节点的信息,以及颜色信息。

插入节点的流程:

  1. 首先定义两个临时节点,
    1. node:初始化为之root,利用node!=null 不断循环往下找节点,
    2. parent:初始化为null,循环时parent存node往下查找之前的值,如果是修改,这个值没有用,如果是新增,这个parent就是要插入节点的父节点
  2. 循环往下找,如果是修改,就直接改value然后返回,如果是新增,就继续下面的步骤
  3. 定义新增的节点,维护新节点和parent之间的关系
  4. 进行颜色调整(红红不能相邻,各个路径黑色节点数目相同)
    public void put(int key, Object value) {
        RBNode node = root;
        RBNode parent = null;
        // 从根节点开始往下找
        while (node != null) {
            // parent暂存当前节点,也就是往下找的子节点的父节点,下面的分支是找子节点
            parent = node;
            if (key < node.key) {
                // key 小于当前节点,往左找
                node = node.left;
            } else if (key > node.key) {
                // key 大于当前节点,往右找
                node = node.right;
            } else {
                // key相同,直接修改value就可以返回了
                node.value = value;
                return;
            }
        }

        // 走到这一步,已经查到了新增的节点要插入到哪个节点下面了,也就是parent节点
        // 定义新的红黑树节点
        RBNode insert = new RBNode(key, value);
        // parent为null,说明上面的while循环根本没进去,说明这个节点是红黑树的根节点,赋值成根节点即可
        if (parent == null) {
            root = insert;
        } else if (key < parent.key) {
            // 往左插入,维护好插入节点和父节点之间的关系
            parent.left = insert;
            insert.parent = parent;
        } else {
            // 往右插入,维护好当前节点和父节点之间的关系
            parent.right = insert;
            insert.parent = parent;
        }

        // 调整颜色信息
        fixRedRed(insert);
    }

每次插入操作的时候,都需要维护颜色信息,保证符合红黑树的特性:

  1. 插入的节点是根节点 ------ 首次插入节点,直接把根节点变成黑色就可以。
  2. 插入的节点父节点是黑色 ------- 树的红黑树性质不会改变,直接插入即可
  3. 插入的节点父亲是红色节点 -------- 出现红红相邻的情况,此时就需要调整红黑树了
    1. 叔叔节点是红色 ---  平衡方法如下:
      1. 父亲节点和叔叔节点都变成黑色,祖父节点变成红色
      2. 此时需要调整祖父节点的颜色信息了,其实就是递归调用,传参树祖父节点
    2. 叔叔节点是黑色 --- 四种情况
      1. LL:父亲节点是左孩子,插入的节点也是左孩子
        1. 父节点变黑,祖父节点变红,以祖父节点为根,执行一次右旋操作
      2. LR:父亲节点是左孩子,插入的节点是右孩子
        1. 以父亲节点为根执行一次左旋操作--- 此时得到了LL结构
        2. 新的父亲节点变黑(插入的节点),祖父节点变红,以祖父节点为根,执行一次右旋操作
      3. RR:父亲节点是右孩子,插入的节点也是右孩子
        1. 父节点变黑,祖父节点变红,以祖父节点为根,执行一次左旋操作
      4. RL:父亲节点是右孩子,插入的节点是左孩子
        1. 以父亲节点为根执行一次右旋操作--- 此时得到了RR结构
        2. 新的父亲节点变黑(插入的节点),祖父节点变红,以祖父节点为根,执行一次左旋操作
    private void fixRedRed(RBNode x) {
        // 1、插入的是根节点,变黑返回
        if (x == root) {
            x.color = "black";
            return;
        }
        // 2、父节点为黑色,插入红色节点不影响平衡,直接返回
        if (isBlack(x.parent)) {
            return;
        }

        // 父亲节点为红色,这种会产生红红相邻,需要进行调整
        RBNode uncle = x.uncle();
        RBNode parent = x.parent;
        RBNode grandparent = parent.parent;

        //3、父亲节点为红色,叔叔节点是红色 --- 说明祖父节点是黑色的
        // 为了保证红红不相邻,只要将父亲节点变成黑色就满足了
        // 为了保证每条路径黑色节点数目相同,需要在将叔叔节点变成黑色,祖父节点变成红色
        // 变完之后,祖父节点和它的父节点可能红红相邻呀,那就递归调用祖父节点进行颜色平衡就可以了。
        if (isRed(uncle)) {
            parent.color = "black";
            uncle.color = "black";
            grandparent.color = "red";
            fixRedRed(grandparent);
            return;
        }

        //4、 父亲节点为红色,叔叔节点是黑色(其实这种情况是没有叔叔节点,因为如果存在的话,就黑色节点数目不平衡了)
        if (parent.isLeftChild()) { // 父亲左孩子
            if (x.isLeftChild()) { // 插入节点在左边
                // LL
                // 将父亲节点变成黑色,祖父节点变成红色,然后以祖父节点为根节点进行右旋即可
                parent.color = "black";
                grandparent.color = "red";
                rightRotate(grandparent);
            } else { // 插入节点在右边
                // LR
                // 先左旋父节点,变成LL, 然后按照LL进行解决
                // 注意,左旋后,x已经变成父节点了,因此是x变黑,而不是parent变黑
                leftRotate(parent);
                x.color = "black";
                grandparent.color = "red";
                rightRotate(grandparent);
            }
        } else { // 父亲右孩子
            if (!x.isLeftChild()) { // 插入节点在右边
                // RR
                // 同LL,将父亲节点变成黑色,祖父节点变成红色,然后以祖父节点为根节点进行左旋即可
                parent.color = "black";
                grandparent.color = "red";
                leftRotate(grandparent);
            } else { // 插入节点在左边
                // RL 先右旋父节点,变成RR
                rightRotate(parent);
                x.color = "black";
                grandparent.color = "red";
                leftRotate(grandparent);
            }
        }
    }

红黑树节点的删除

  1. 删除的节点是叶子节点--> 直接删,然后平衡颜色
  2. 删除的节点有一个孩子 -->
    1. 如果存在左孩子,把左孩子挂到当前位置,然后平衡颜色
    2. 如果存在右孩子,把右孩子挂到当前位置,然后平衡颜色
  3. 删除的节点有两个孩子 --> 先找到待删除节点的后继节点,然后把key value 互相替换,这样就转变为了删除后继节点了,即1和2 两种场景

颜色的平衡(难点) --- 记得颜色平衡的前置条件,那就是要删除的节点,要么是叶子节点,要么只有一个孩子,因为两个孩子的节点已经被转化了。

  1. 删除的是红色节点,
    1. 叶子节点:不需要平衡,直接删就行
    2. 非叶子节点:根据红黑树平衡要求,此时该节点必然有两个黑孩子,那就转为删除该节点的后继节点,后继节点不会有两个孩子
  2. 删除的是黑色节点
    1. 是根节点,直接删就行
    2. 兄弟节点是红色:---> 说明此时父节点,以及侄子节点全都是黑色,且必然有侄子节点
      1. 兄弟节点在左侧:对父节点进行右旋,旋转后交换父节点和兄弟节点颜色
      2. 兄弟节点在右侧:对父节点进行右旋,旋转后交换父节点和兄弟节点颜色
      3. 旋转后交换父节点和兄弟节点颜色是为了保持黑色节点平衡,这样就转为了要删除的节点的兄弟节点是黑色了
    3. 兄弟节点是黑色: 
      1. 兄弟节点两个都是黑孩子
        1. 兄弟变红:删除节点的一方和兄弟的一方黑色高度同时减一
        2. 父亲为红色:直接变成黑色,此时已经达到平衡
        3. 父亲节点为黑色:此时这个路径上黑色减一,让父节点触发颜色平衡的操作
      2. 兄弟节点至少有一个是红孩子
        1. 兄弟是左孩子,
          1. 左侄子是红色,LL不平衡 :父节点右旋一次,然后,父节点和兄弟节点交换颜色(这一步是保证原父节点位置的颜色不变),兄弟节点的左孩子变黑(原兄弟节点的位置还是黑色)
          2. 右侄子是红色,LR不平衡:(这一步,说明左孩子不是红,但是又不可能为黑,因此没有左孩子),兄弟节点先左旋一次(兄弟节点左旋就下去了,对应的路径上黑色节点数目不变),父亲节点右旋一次,然后父节点的颜色赋给右侄子(父节点的位置颜色不变),父节点变黑(要删除的黑节点的位置还是赋成黑色)
        2. 兄弟是右孩子,和上面是对称的
          1. RR不平衡
          2. RL不平衡
public void remove(int key) {
    RBNode delete = findNode(key);
    // 没有找到要删除的节点,直接返回
    if (delete == null) {
        return;
    }
    doRemove(delete);
}

// 删除红色节点,不会改变路径中的黑色节点个数,不需要平衡
// 删除黑色节点,会改变路径中黑色节点个数,需要进行平衡
private void doRemove(RBNode delete) {
    RBNode replaced = findReplaced(delete);
    RBNode parent = delete.parent;
    // 待删除节点没有孩子
    if (replaced == null) {
        //  如果是根节点,直接将root置为空
        if (delete == root) {
            root = null;
        } else {
            if (isBlack(delete)) {
                // 删除的是黑色叶子节点
                fixBlackBlack(delete);
            }
            // 删除红色叶子节点,不需要处理,因此这里没有对应的else

            // 不是根节点,就将父节点对应的子节点置为空
            if (delete.isLeftChild()) {
                parent.left = null;
            } else {
                parent.right = null;
            }
            delete.parent = null;
        }
        return;
    }

    // 待删除节点有一个孩子
    if (delete.left == null || delete.right == null) {
        if (delete == root) {
            // 要删除节点是根节点,直接把要删除节点的key和value赋给root就行了,
            // 因为只有一个孩子,变完之后的root左右孩子都置为空就可以了
            root.key = delete.key;
            root.value = delete.value;
            root.left = root.right = null;
        } else {
            if (delete.isLeftChild()) {
                parent.left = replaced;
            } else {
                parent.right = replaced;
            }
            replaced.parent = parent;
            delete.left = delete.right = delete.parent = null;

            if (isBlack(delete)) {
                if (isBlack(replaced)) {
                    // 因为要删除的节点变成replaced了,因此调整的也是replaced
                    fixBlackBlack(replaced);
                } else {
                    // 删的是黑,剩下的是红色,只要把剩下的变黑就可以了
                    replaced.color = "black";
                }
            }
        }
        return;
    }

    // 后面的逻辑就是待删除节点有两个孩子了
    // 可以将带删除节点和他的后继节点 的key和value进行替换,这样就转变成了删除后继节点
    int key = delete.key;
    delete.key = replaced.key;
    replaced.key = key;

    Object value = delete.value;
    delete.value = replaced.value;
    replaced.value = value;

    // 调换完之后,就可以变成删除replaced节点了,这样就变成了待删除节点没有孩子或者只有一个孩子。
    doRemove(replaced);
}

// 要删除的节点是黑色,余下的节点也是黑色(只有一个孩子,或者没有孩子的场景)
// 

/**
 * 要删除的节点是黑色,余下的节点也是黑色(只有一个孩子,或者没有孩子的场景)
 * 1、调整节点的 兄弟节点是红色,此时两个侄子节点必为黑色
 * 2、调整节点的 兄弟节点是黑色,两个侄子也都是黑色
 * 3、调整节点的 兄弟节点是黑色,至少有一个侄子节点为红色
 */
private void fixBlackBlack(RBNode node) {
    // 
    if (node == root) {
        return;
    }

    RBNode parent = node.parent;
    RBNode brother = node.brother();

    // 调整节点的 兄弟节点是红色,此时两个侄子节点必为黑色,父节点也肯定是黑色
    if (isRed(brother)) {
        // 左孩子就进行左旋,右孩子就进行右旋
        if (node.isLeftChild()) {
            leftRotate(parent);
        } else {
            rightRotate(parent);
        }
        // 旋转完成之后,父亲节点和兄弟节点互换颜色,这样可以保证红黑树平衡
        parent.color = "red";
        brother.color = "black";

        // 此时是场景2或者3了,递归调用
        fixBlackBlack(node);
        return;
    }

    /**
     * 调整节点的兄弟节点是黑色,两个侄子都为黑色
     * a,调整兄弟节点为红色,目的是将删除节点和兄弟节点两边树的黑色高度都减一
     * b,如果父亲是红色,那么直接将父亲节点变成黑色就可以了
     * c,如果父亲节点是黑色,说明这个路径少了一个黑色节点,让父亲节点触发双黑(循环调用)
     */
    if (isBlack(brother.left) || isBlack(brother.right)) {
        brother.color = "red";
        if (isRed(parent)) {
            parent.color = "black";
        } else {
            fixBlackBlack(parent);
        }
    }
    /**
     * 调整节点的兄弟节点是黑色,两个侄子至少有一个红色 -- 因为之前树是平衡的,所有有侄子,必定是红色的
     * a,兄弟是左孩子,并且有左侄子  LL
     * b,兄弟是左孩子,并且有右侄子  LR  -- 同时有两个侄子也属于这种
     * c,兄弟是右孩子,并且有左侄子  RL
     * d,兄弟是右孩子,并且有右侄子  RR  -- 同时有两个侄子也属于这种
     */
    else {
        if (brother.isLeftChild()) {
            if (isRed(brother.left)) {
                // 画图,就理解了,这个分支,无论右侄子是否存在,是否是红色,都可以用同一块代码
                // 先右旋,然后父节点和兄弟节点交换颜色 ---- 保证原父节点位置的颜色不变
                // 兄弟节点的左孩子变黑-- 原兄弟节点的位置还是黑色
                rightRotate(parent);
                brother.color = parent.color;
                parent.color = "black"; // 因为兄弟节点是黑色,这里直接赋给parent就行了
                brother.left.color = "black";
            } else if (isRed(brother.right)) {
                brother.right.color = parent.color; // 左旋后brother的右孩子会变,所以在这里先变色
                leftRotate(brother);
                rightRotate(parent);
                parent.color = "black";
            }
        } else {
            if (isRed(brother.right)) {
                leftRotate(parent);
                brother.color = parent.color;
                parent.color = "black";
                brother.right.color = "black";
            } else if (isRed(brother.left)) {
                brother.left.color = parent.color;
                rightRotate(brother);
                leftRotate(parent);
                parent.color = "black";
            }
        }
    }
}

// 查找要删除的节点
private RBNode findNode(int key) {
    RBNode node = root;
    while (node != null) {
        if (key < node.key) {
            node = node.left;
        } else if (key > node.key) {
            node = node.right;
        } else {
            return node;
        }
    }
    return null;
}

// 查找后面放在要删除节点位置的节点
private RBNode findReplaced(RBNode delete) {
    //  删除节点是叶子节点,没有后续节点,返回null
    if (delete.left == null && delete.right == null) {
        return null;
    }
    // 删除节点没有左孩子,直接返回右孩子
    if (delete.left == null) {
        return delete.right;
    }
    // 删除节点没有右孩子,直接返回左孩子
    if (delete.right == null) {
        return delete.left;
    }

    // 左右孩子都存在,那就去找这个节点的后继节点
    RBNode right = delete.right;
    while (right.left != null) {
        right = right.left;
    }
    return right;
}

个人心得:

对于红黑树的颜色平衡,记住以下原则:

  1. 能够直接变色,就直接变色
  2. 不能变色,就旋转,旋转过程要伴随着变色,保证旋转前关键位置的颜色,在旋转后还是原来的颜色
  3. 如果当前路径下黑色节点数目一致了,但是和其他路径有差,那就递归调用父节点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值