每日分享程序员掌握的知识之:非线性结构!红黑树

1.2.5 红黑树

红黑树,Red-Black Tree 「RBT」是一个自平衡(不是绝对的平衡)的二叉查找树(BST),树上的每个节点都遵循下面的规则:

1 每个节点要么是黑色,要么是红色。

2 根节点是黑色。

3 每个叶子节点(NIL)是黑色。

4 每个红色结点的两个子结点一定都是黑色。

5 任意一结点到每个叶子结点的路径都包含数量相同的黑结点。

红黑树能自平衡,它靠的是什么?三种操作:左旋、右旋和变色

 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,<br>右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。

右旋 以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,<br/>左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。

变色 结点的颜色由红变黑或由黑变红。

旋转操作

左旋:以某个节点作为旋转点,其右子节点变为旋转节点的父节点,右子节点的左子节点变为旋转节点的右子节点,左子节点保持不变。

右旋:以某!个节点作为旋转点,其左子节点变为旋转节点的父节点,左子节点的右子节点变为旋转节点的左子节点,右子节点保持不变。

Java代码实现旋转: 先进行类结构定义 

package com.bobo.util.treemap;

public class BRTree {

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

    private RBNode root;

    public RBNode getRoot() {
        return root;
    }

    public void setRoot(RBNode root) {
        this.root = root;
    }


    /**
     * 表示 节点
     * @param <K>
     * @param <V>
     */
    static class RBNode<K extends Comparable<K>,V>{
        // 节点是双向的
        private RBNode parent;
        private RBNode left;
        private RBNode right;
        private boolean color;
        private K key;
        private V value;

        public RBNode() {
        }

        public RBNode(RBNode parent, RBNode left, RBNode right, boolean color, K key, V value) {
            this.parent = parent;
            this.left = left;
            this.right = right;
            this.color = color;
            this.key = key;
            this.value = value;
        }

        public RBNode getParent() {
            return parent;
        }

        public void setParent(RBNode parent) {
            this.parent = parent;
        }

        public RBNode getLeft() {
            return left;
        }

        public void setLeft(RBNode left) {
            this.left = left;
        }

        public RBNode getRight() {
            return right;
        }

        public void setRight(RBNode right) {
            this.right = right;
        }

        public boolean isColor() {
左旋代码实现
    /**
     * 围绕p左旋
     *     p               pr(r)
     *    / |             / \
     *   pl  pr(r) =>    p   rr
     *      / \         / \
     *     rl  rr     pl  rl
     *
     *  左旋的时候
     *  p-pl 和 pr-rr的关系不变
     *  pr-rl 要变为 p-rl
     *      也就是 rl要变为 p的右子节点
     *      同时 p要成为 rl 的父节点
     *  还有就是要判断  p 是否有父节点
     *  如果没有
     *     r 变为 root 节点
     *  如果有
     *     r.parent = p.parent
     *     还要设置 r为 p.parent 的子节点(可能左也可能右)
     *     如果 p.parent.left == p
     *        p.parent.left = r;
     *     否则
     *        p.parent.right = r;
     *    最后
     *       p.parent = r;
     *       r.left = p;
     * @param p
     */
    private void leftRotate(RBNode p){
        if(p != null){
            RBNode r = p.right;
            // 1.设置 pr-rl 要变为 p-rl
            // 把rl设置到p的右子节点
            p.right = r.left;
            if(r.left != null){
                // 设置rl的父节点为p
                r.left.parent = p;
            }
            // 2.判断p的父节点情况
            r.parent = p.parent; // 不管 p是否有父节点,都把这个父节点设置为 r的父节点
            if(p.parent == null){
                root = r; // p没有父节点 则r为root节点
            }else if(p.parent.left == p){
                p.parent.left = r; // 如果p为 p.parent的左子节点 则 r 也为 p.parent的左子节点
            }else{
                p.parent.right = r; // 反之设置 r 为 p.parent的右子节点
            }
            // 最后 设置 p 为 r 的左子节点
            r.left = p;
            p.parent = r;
        }
    }
右旋实现:
    /**
     * 围绕p右旋
     * @param p
     */
    public void rightRotate(RBNode p){
        if(p != null){
            RBNode r = p.left;
            p.left = r.right;
            if(r.right != null){
                r.right.parent = p;
            }
            r.parent = p.parent;
            if(p.parent == null){
                root = r;
            }else if(p.parent.left == p){
                p.parent.left = r;
            }else{
                p.parent.right = r;
            }
            r.right = p;
            p.parent = r;
        }

新增节点:https://www.processon.com/view/link/60c21e25e401fd34a1514d25

2-3-4树中结点添加需要遵守以下规则:

● 插入都是向最下面一层插入

● 升元:将插入结点由 2-结点升级成 3-结点,或由 3-结点升级成 4-结点;

● 向 4-结点插入元素后,需要将中间元素提到父结点升元,原结点变成两个 2-结点,再把元素插入2-结点中,如果父结点也是 4-结点,则递归向上层升元,至到根结点后将树高加1;

而将这些规则对应到红黑树里,就是:

● 新插入的结点颜色为 红色 ,这样才可能不会对红黑树的高度产生影响。

● 2-结点对应红黑树中的单个黑色结点,插入时直接成功(对应 2-结点升元)。

● 3-结点对应红黑树中的 黑+红 子树,插入后将其修复成 红+黑+红 子树(对应 3-结点升元);

● 4-结点对应红黑树中的 红+黑+红 子树,插入后将其修复成 红色祖父+黑色父叔+红色孩子 子树,然后再把祖父结点当成新插入的红色结点递归向上层修复,直至修复成功或遇到 root 结点;

公式:红黑树+新增一个节点(红色)=对等的2-3-4树+新增一个节点

新增节点案例 我们通过新增2-3-4树的过程来映射对应的红黑树的节点新增

1.新增一个节点,2 节点

2.新增一个节点,与2节点合并,直接合并

3.新增一个节点,与3节点合并,直接合并 插入的值的位置会有3种情况

4.新增一个节点,与4节点合并,此时需要分裂

插入值的位置可能是

对应的红黑树的结构为:

新增代码实现
  红黑树的新增规则我们理清楚了,接下来就可以通过Java代码来具体的实现了。
先实现插入节点,这就是一个普通的二叉树的插入
 /**
     * 新增节点
     * @param key
     * @param value
     */
    public void put(K key , V value){
        RBNode t = this.root;
        if(t == null){
            // 说明之前没有元素,现在插入的元素是第一个
            root = new RBNode<>(key , value == null ? key : value,null);
            return ;
        }
        int cmp ;
        // 寻找插入位置
        // 定义一个双亲指针
        RBNode parent;
        if(key == null){
            throw new NullPointerException();
        }
        // 沿着跟节点找插入位置
        do{
            parent = t;
            cmp = key.compareTo((K)t.key);
            if(cmp < 0){
                // 左侧找
                t = t.left;
            }else if(cmp > 0){
                // 右侧找
                t = t.right;
            }else{
                // 插入节点的值==比较的节点。值替换
                t.setValue(value==null?key:value);
                return;
            }
        }while (t != null);
        // 找到了插入的位置  parent指向 t 的父节点  t为null
        // 创建要插入的节点
        RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
        // 然后判断要插入的位置 是 parent的 左侧还是右侧
        if(cmp < 0){
            parent.left = e;
        }else{
            parent.right = e;
        }
        // 调整  变色 旋转
        fixAfterPut(e);
    }

然后再根据红黑树的特点来实现调整(旋转,变色)

private boolean colorOf(RBNode node){
        return node == null ? BLACK:node.color;
    }

    private RBNode parentOf(RBNode node){
        return node != null ? node.parent:null;
    }

    private RBNode leftOf(RBNode node){
        return node != null ? node.left:null;
    }

    private RBNode rightOf(RBNode node){
        return node != null ? node.right:null;
    }

    private void setColor(RBNode node ,boolean color){
        if(node != null){
            node.setColor(color);
        }
    }

    /**
     * 插入节点后的调整处理
     * 1. 2-3-4树 新增元素 2节点添加一个元素将变为3节点 直接合并,节点中有两个元素
     *      红黑树:新增一个红色节点,这个红色节点会添加在黑色节点下(2节点) --- 这种情况不需要调整
2. 2-3-4树 新增元素 3节点添加一个元素变为4节点合并 节点中有3个元素
     *      这里有6中情况,( 根左左 根左右  根右右 根右左)这四种要调整  (左中右的两种)不需要调整
     *      红黑树:新增红色节点 会添加到 上黑下红的节点中 = 排序后中间节点是黑色,两边节点是红色
     *
     *  3. 2-3-4树:新增一个元素 4节点添加一个元素需要裂变:中间元素升级为父节点,新增元素与剩下的其中一个合并
     *      红黑树:新增节点是红色+爷爷节点是黑色,父亲节点和叔叔节点为红色 调整为
     *              爷爷节点变红色,父亲和叔叔节点变为黑色,如果爷爷节点为root节点则调整为黑色
     * @param x
     */
    private void fixAfterPut(RBNode<K, Object> x) {
        x.color = RED;
        // 本质上就是父节点是黑色的就不需要调整,对应的 2 3的情况
        while(x != null && x != root && x.parent.color == RED){
            // 1. x 的父节点是爷爷的 左孩子
            if(parentOf(x) == parentOf(parentOf(x)).left){
                // 获取当前节点的叔叔节点
                RBNode y = rightOf(parentOf(parentOf(x)));
                // 情况3
                if(colorOf(y) == RED){
                    // 说明是 上3的情况  变色处理
                    // 父亲节点和叔叔节点设置为黑色
                    setColor(parentOf(x),BLACK);
                    setColor(y,BLACK);
                    // 爷爷节点设置为 红色
                    setColor(parentOf(parentOf(x)),RED);
                    // 递归处理
                    x = parentOf(parentOf(x));
                }else{
                    // 情况 2
                    if(x == parentOf(x).right){
                        // 如果x是父节点的右节点那么我们需要先根据 父节点 左旋
                        x = parentOf(x);
                        leftRotate(x);
                    }
                    // 叔叔节点为空  对应于 上面的情况2
                    // 将父节点变为黑色
                    setColor(parentOf(x),BLACK);
                    // 将爷爷节点变为红色
                    setColor(parentOf(parentOf(x)),RED);
                    // 右旋转  根据爷爷节点右旋转
                    rightRotate(parentOf(parentOf(x)));

                }
            }else{
                // x 的父节点是爷爷是右孩子
                // 获取父亲的叔叔节点
                RBNode y = leftOf(parentOf(parentOf(x)));
                if(colorOf(y) == RED){
                    // 情况3
                    setColor(parentOf(x),BLACK);
                    setColor(y,BLACK);
  • 红黑树的删除操作:

  • 红黑树的节点的删除其实也分为两步:

  • 1 先删除节点(这步和普通的二叉树删除是一样的)

  • 2 然后再调整 要删除这个节点先需要找到这个节点,找到节点就是普通的二分查找,

  • 具体代码如下

     private RBNode getNode(K key){
            RBNode node = this.root;
            while (node != null ){
                int cmp = key.compareTo((K) node.key);
                if(cmp < 0){
                    // 在左子树
                    node = node.left;
                }else if(cmp >0){
                    // 右子树
                    node = node.right;
                }else{
                    return node;
                }
            }
            return null;
        }

    在整理红黑树节点的删除操作时我们需要先理解清楚红黑树删除和2-3-4树删除的等价关系,这样理解起来才会比较容易 核心理论:红黑树删除操作的本质其实就是删除2-3-4树的叶子节点

  • 情况一

  • 情况2:删除的是非情况1的节点,根据我们前面介绍的删除的规则,会找到对应的前驱和后继节点,那么最终删除的还是叶子节点

首先删除节点的代码为:

/**
     * 删除节点
     * @param key
     * @return
     */
    public V remove(K key){
        // 先找到这个节点
        RBNode node = getNode(key);
        if(node == null){
            return null;
        }
        // 把值存起来 删除后 返回
        V oldValue = (V) node.value;
        deleteNode(node);
        return oldValue;
    }

    /**
     * 删除节点
     *   3种情况
     * 1.删除叶子节点,直接删除
     * 2.删除的节点有一个子节点,那么用子节点来替代
     * 3.如果删除的节点右两个子节点,此时需要找到前驱节点或者后继节点来替代
     *    可以转换为 1、2的情况
     * @param node
     */
    private void deleteNode(RBNode node){
        // 3.node节点有两个子节点
        if(node.left !=null && node.right != null){
            // 找到要删除节点的后继节点
            RBNode successor = successor(node);
            // 然后用后继节点的信息覆盖掉 要删除节点的信息
            node.key = successor.key;
            node.value = successor.value;
            // 然后我们要删除的节点就变为了 后继节点
            node = successor;
        }
        // 2.删除有一个子节点的情况
        RBNode replacement = node.left != null ? node.left : node.right;
        if(replacement != null){
            // 替代者的父指针指向原来 node 的父节点
            replacement.parent = node.parent;
            if(node.parent == null){
                // 说明 node 是root节点
                root = replacement;
            }else if(node == node.parent.left){
                // 双向绑定
                node.parent.left = replacement;
            }else{
                node.parent.right = replacement;
            }
            // 将node的左右孩子指针和父指针都指向null node等待GC
            node.left = node.right = node.parent = null;
            // 替换完成后需要调整平衡
            if(node.color == BLACK){
                // fixAfterRemove(replacement)
            }
        }else if(node.parent == null){
            // 说明要删除的是root节点
            root = null;
        }else{
            // 1. node节点是叶子节点 replacement为null
            // 先调整
            if(node.color == BLACK){
                // fixAfterRemove(node)
            }
            // 再删除
            if(node.parent != null){
                if(node == node.parent.left){
                    node.parent.left = null;
                }else{
                    node.parent.right = null;
                }
                node = null;
            }
        }
    }

然后就是需要调整红黑树的平衡了。

删除后的平衡调整

1.情况一:自己能搞定的,对应叶子节点是3节点和4节点

2.情况二:自己搞不定,需要兄弟借,但是兄弟不借,找父亲借,父亲下来,然后兄弟找一个人去代替父亲当家

这种情况就是兄弟节点是3节点或者4节点

找兄弟节点

如果找到的兄弟节点是红色其实还要调整

执行如下调整先,先变色,然后左旋

找兄弟节点借

然后沿着7节点左旋

3.情况三:跟兄弟借,兄弟也没有(情同手足,同时自损) 兄弟节点是2节点,同时当前节点的父节点是红色节点的情况

删除后直接变色就可以了 兄弟节点是2节点,同时当前节点的父节点是黑色节点

变更操作为如下,如果继续有父节点那么还要递归处理

分析清楚了删除的3中情况,我们就可以撸处删除的调整的代码了

/**
     * 2-3-4树删除操作:
     * 1.情况一:自己能搞定的,对应叶子节点是3节点和4节点
     * 2.情况二:自己搞不定,需要兄弟借,但是兄弟不借,找父亲借,父亲下来,然后兄弟找一个人去代替父亲当家
     * 3.情况三:跟兄弟借,兄弟也没有
     * @param x
     */
    private void fixAfterRemove(RBNode x){
        // 情况2、3
        while(x != root && colorOf(x) == BLACK){
            // 这种情况才需要调整
            // x 是左孩子的情况
            if(x == leftOf(parentOf(x))){
                // 找兄弟节点
                RBNode rNode = rightOf(parentOf(x));
                // 判断此时的兄弟节点是否是真正的兄弟节点 兄弟是红色的情况要调整
                if(colorOf(rNode) == RED){ // 2-3-4树的 3节点 交换颜色,然后左旋一次就可以了
                    setColor(rNode,BLACK);
                    setColor(parentOf(x),RED);
                    leftRotate(parentOf(x)); // 左旋一次
                    rNode = rightOf(parentOf(x)); // 找到真正的兄弟节点
                }
                // 情况3 找兄弟借 没得借
                if(colorOf(leftOf(rNode)) == BLACK && colorOf(rightOf(rNode)) == BLACK){
                    // 情况复杂
                    setColor(rNode,RED);
                    x=parentOf(x); // 向上递归
                }else{
                    // 情况2 找兄弟借 有借
                    // 兄弟节点是 3节点或者4节点
                    if(colorOf(rightOf(rNode)) == BLACK){
                        // 右孩子为空,则左孩子肯定不为空
                        // 兄弟节点 先要左一次右旋
                        setColor(rNode,RED);
                        setColor(leftOf(rNode),BLACK);
                        rightRotate(rNode);
                        // 重新调整叔叔节点的位置
                        rNode = rightOf(parentOf(x));
                    }
                    // 变色  兄弟节点是 3节点还是4节点 都旋转一次
                    setColor(rNode, colorOf(parentOf(x)));
                    setColor(parentOf(x),BLACK);
                    setColor(rightOf(rNode),BLACK);
                    // 左旋
                    leftRotate(parentOf(x));
                    x = root; // 结束循环  递归 针对的是 情况3
                }
            }else{
                // 找兄弟节点
                RBNode rNode = leftOf(parentOf(x));
                // 判断此时的兄弟节点是否是真正的兄弟节点 兄弟是红色的情况要调整
                if(colorOf(rNode) == RED){ // 2-3-4树的 3节点 交换颜色,然后左旋一次就可以了
                    setColor(rNode,BLACK);
                    setColor(parentOf(x),RED);
                    rightRotate(parentOf(x)); // 左旋一次
                    rNode = leftOf(parentOf(x)); // 找到真正的兄弟节点
                }
                // 情况3 找兄弟借 没得借
                if(colorOf(rightOf(rNode)) == BLACK && colorOf(leftOf(rNode)) == BLACK){
                    // 情况复杂
                    setColor(rNode,RED);
                    x=parentOf(x); // 向上递归
                }else{
                    // 情况2 找兄弟借 有借
                    // 兄弟节点是 3节点或者4节点
                    if(colorOf(leftOf(rNode)) == BLACK){
                        // 右孩子为空,则左孩子肯定不为空
                        // 兄弟节点 先要左一次右旋
                        setColor(rNode,RED);
                        setColor(leftOf(rNode),BLACK);
                        leftRotate(rNode);
                        // 重新调整叔叔节点的位置
                        rNode = leftOf(parentOf(x));
                    }
                    // 变色  兄弟节点是 3节点还是4节点 都旋转一次
                    setColor(rNode, colorOf(parentOf(x)));
                    setColor(parentOf(x),BLACK);
                    setColor(leftOf(rNode),BLACK);
                    // 左旋
                    rightRotate(parentOf(x));
                    x = root; // 结束循环  递归 针对的是 情况3
                }
            }
        }
        // 情况1:替代节点是红色,直接染黑  在情况3的情况下  补偿删除的黑色节点,这样红黑树依然保存平衡
        setColor(x,BLACK);
    }

当然不会就此结束,还有更多的Java场景题,因为篇幅原因,无法给大家全部展示出来,有需要的看板老爷们,

查看下方小名片即可直接白嫖拿走哦!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值