树之手撕红黑树,深入B/B+树

简单说一下二叉搜索树与AVL树

要学红黑树,首先你必须学会二叉搜索树,也就是二叉查找树,如果不会的同学,可以去看我写过的文章里面有

那么这里我们来说一下AVL树

他就是一个平衡二叉搜索树,什么叫平衡呢,就是一棵树维持一个稳定的二叉状态,维持一个稳定的高度,高度稳定了,它的查找时间复杂度也就低了,那么AVL树就是一个利用左右子树高度之差小于等于1的这样一个平衡因子来维持的树的平衡,一旦左右子树高度之差大于1,那么树就是开始旋转。

下面来简单对比一下二叉搜索树和AVL树

那么为了解决AVL树严格的旋转问题,红黑树因此由运而生。

 红黑叔:也是平衡二叉树,只是他没有AVL树那么严格,它的平衡因子是通过每一条路径上黑色结点数目一致来决定的,也就是黑色结点高度一致决定的

他在插入数据的过程中,就不用那么多次的旋转来插入数据。

这里简单说一下B树与B+树的存储结构

B树:

我们在学习的过程中,可能还会碰到另外一些平衡查找树,比如说2-3树,2-3-4树,这里简单提一下2-3树,2-3-4树类似

当然你可以只包含2节点,只包含3结点,但一般不那么来做,它好像就是一种特殊的3阶B树,其实也就是可以看成三阶B树,只要这棵树符合定义的话。

说回来继续说B树,下面我们来看一个B树的结构

B树中数据的插入过程

下面我换一种可视化来表示,下面我们再继续插

这里插入33,是插到双亲结点里面去了,树的高度没有增加,像我们在插入53的时候,没有双亲结点,就会新建一个双亲结点,简而言之树的高度会在增加一层。而且我们会发现,所有的插入都会先从叶子结点试图插入,空间不够,然后才从中间结点开始向上分裂。

B树中数据删除的过程

删除分为两步:1.找到这个结点,2.然后删除

删除又分为两种情况考虑:第一种是叶子结点删除;第二种是非叶子结点删除

先来说非叶子结点删除:当一个结点删除之后,我们要看它当前结点是否满足第一点是最小的min_key值,也就是ceil(m)/2的值,如果不满足最小的min,我们就要去借,找左右子结点里面一个左边最大,右边最小的去借,借了之后判断是否构成一个b树,如果不构成,左右子结点又去借父亲的最小值,然后考虑是否合并,这里的合并包括了当前删除结点的合并或者子结点的合并。

这里说一下删除结点需不要需要向上合并,也就是下面这个

合并可以减少高度的情况,我们是考虑为合并的 

那么如果另外一种情况是删了之后还是满足B树的min,我们就要考虑是否构成左小右大,如果不构成,就从左右子结点当中最多的一组里面找一个最小值或者最大值放上去,满足左小又大嘛。

我们来看一个插入过程:m = 5,min= 2

我们要删除18这样一个非叶子结点 ,删除之后,明显不满足最小的min

删除之后不满足,我们去左结点里面借了一个17放上去,这个时候子结点又不满足min

然后我子结点去借一个父亲结点的最小值13,然后合并

 这个时候满足了B树的规则,但是我们如果删除结点的合并可以减少树的高度,那么我们把删除结点进行如下合并

再来说叶子结点的删除:

第一种情况是:如果删除的结点的key > min,那么删除就不影响,直接删了就行。

第二种情况是:从父亲结点去借一个下来,然后看需不要需要合并,如果需要合并(这里合并也并不一定是不满足min,也可能就是一个左小右大的分叉,合理性问题),就把子结点合并,然后如果删除结点不满足min,就去考虑是否再去子结点借还是合并,如果两个情况都满足,那么合并如果能减少树的高度,我们直接考虑为合并

具体考虑如下:

我们上面要删除4这个叶子结点,那么很明显删了之后不满足min ,就要去父亲借,借一个最小的下来,然后合并,这就是合理性,1235都是小于6的,所以直接合并。

 

上面这种情况我们考虑6 10 13 18的合并,这样可以降低树的高度

但是也可以如下考虑

但是这种情况树的高度不会变 

一个简单的删除实例:

m=5 ,min = 2

上面就是介绍了B树的在内存中的逻辑结构

下面我来说说B树的特点:

B树就是所有结点都存储数据

下面来说一下B+树

树的设计原则也和B树一样,插入与删除思路一致

下面我们说一下B+树它优化后的两个点

1.它的内部结点只存储部分索引,用于导航

2.内部结点包括根结点都不存储任何data数据,也就是磁盘上的数据,这样子来说,对于同一个m阶的B树来说,B+树每个结点能存储更多的key,换句话说,在某种情况下,它的树没有B树那么高,检索次数也就越少,那么磁盘IO的次数也就越少

3.对于范围检索来说,B+树的优势相对明显的,当我们找到叶子结点的时候,需要按顺序查找一定的范围,只要在这个磁盘上进行双向链表操作即可,不用进行多余的磁盘io

下面看一下B+树的图:

B树与B+树在磁盘索引上的应用

先来说磁盘

数据全都放在磁盘上,磁盘通过转动(这个大概就是会以一个固定的大小速度进行转动)盘面会去不同的磁道当中的检索数据。

说一下扇区,也可以理解为磁盘块,它是磁盘上面最小的可读写的数据块,通常由硬件或者操作系统定义大小,典型的磁盘大小呢就是512字节,1kb,4kb,在现代的文件系统中,常见磁盘块大小通常为4kb

下面我们说一下页:页可以理解为一个或者多个扇区的集合。它的大小通常为1024个字节或者其整数倍。主存和磁盘是以页为单位进行交换数据,一般我们一次读取一页或者几页载入到内存当中

磁盘的预读原理:将一个结点大小设置为一个页(一个页就是1024个byte或者其整数倍),那么每个结点只需要一次io操作就可以完全载入数据

那么这里对比一下插入10亿个数据,每一个数据是1byte,B树每一个结点大小是4kb=4000byte,每次从内存中拉取一页的大小,B树大概需要多少层,二叉树大概又需要多少层

二叉树

二叉树10亿个数据的检索大约就需要三十层

B树,每一个结点是4000,也就是说能存4000个key ,假设把它全部存满,那么m= 4000 - 1,3999

很明显B树最多3层就把这10个byte字节数据放下来了

B+树它相比于B树它的树会更低,因为他首先内部结点没有全部存储所有的索引,只有一部分用于导航定位,所以它的树不会比B树高,那么每次在磁盘的io就会更少,我们可以看一下磁盘的检索方式

这个是B+树的方式 

这个是B树的检索方式 

 下面我们引入几个常见问题

1.描述一下MySQL里面的B+树的索引原理

 2.Mysql为什么选择使用B+树作为索引而不是B树

1.从树的高度来说,B+树会从两个方面来降低树的高度,从而降低查找的对比次数,第一个是每一个结点不存储数据,那么就会多出来更多的key去存放索引,树会变低,io的次数就会减少,第二个方面就是它内部不会存储全部的索引结点,只会存储部分索引用于导航,因此树也会相对更低,减少io次数

2.B+树的叶子结点是一个有序的双向链表,当我们需要范围查询的时候,我们直接从叶子结点找就可以了,不用再从根去一个节点一个节点对比,当然遍历也会更加高效,直接遍历叶子结点就可以了,而B树呢则需要遍历整棵树

2-3-4树与红黑树的关系

有了上面的知识,去弄懂什么是234树就很容易了

这里为什么要说2-3-4树,是因为红黑树,其实就是由2-3-4树演变而来

而这里的2-3-4树我们就可以看成一个四阶B树,它的插入过程,就符合B树的插入过程,不停从下向上进行分裂与合并。这里就直接拿一个完整的2-3-4树进行讲解

 下面说一下它的2,3,4结点和裂变状态与红黑树的对应关系

因此我们根据等价关系,把上面的2-3-4树变成红黑树就是

 

所以整棵树的形态变成红黑树就是下面的样子

当然根据3结点的左斜右斜还有不同形态,我就不多说了

红黑树的定义

 怎么去理解这几个定义呢?

我们还是结合2-3-4树变红黑树的变换原则来看

不管是2结点,3结点,还是4结点,他们变成红黑树之后,上面的结点都是黑色,所以根结点肯定是黑的

那为什么一个红色结点下面两个子结点一定是黑的呢

那我们把这棵树拉回成一个2-3-4树的样子

我们看7这个结点,如果下面的子孩子结的是二节点,那么啥也不说了,直接就变成黑色结点

那么我们看一下如果下面接的是三结点的情况 

那如果下面结的是四结点的情况呢

 

所以一个红色结点下面肯定会接两个黑色结点

那为什么从某个结点开始到叶子结点上面,每条路径都包含相同数目的黑色结点呢

还是去看变成红黑树的2-3-4树的图

很明显每一层的每一个结点都包含了一个黑色结点 ,所以不管从那一个结点开始它的黑色结点都是一样的

 判断下面的结点哪一个是红黑树,哪一个不是红黑树

 先来看第一棵树是不是红黑树

结点分析: 

 

再来看第二棵树是不是红黑树

 

 树的分析

很明显这棵树就是红黑树

红黑树的研究

我们重点是放在黑色结点上面。红黑树的设计目标之一就是保持黑色高度的平衡,从而确保整棵树的相对平衡。

红黑树的红色节点通过在路径中交替出现,帮助保持了平衡。但在实际分析中,为了简化,通常将注意力集中在黑色高度上

问题:一棵有n个内部结点的红黑树它的高度最多为多少?

先来看一张关于树的图

上面总结归纳两点:

 第一点:

第二点:

于是这个问题就得到解决

 

因此,对于红黑树来说,它的插入,删除,查找,时间复杂度都会在O(logn)的时间复杂度里面,相对于普通的二叉搜索树来说,普通的二叉搜索树最坏的情况下,会退化成一张单链表,因此这个时候的时间复杂度就是O(n)

红黑树的左旋与右旋

下面先来说左旋:

 

再来看一下右旋:

 

通过对比发现,左旋与右旋互为可逆操作。

来一句口诀

看分叉:

左旋看右边,右边看左边,左有放右边 

右旋看左边,左边看右边,右有放左边

下面我们来完成左旋与右旋的代码

再说左旋与右旋之前,我们先来看一下红黑树的结点设计

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(K key, V value, RBNode parent) {
            this.parent = parent;
            this.color = BLACK;
            this.key = key;
            this.value = value;
        }

        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() {
        }

        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() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }

 然后分析一下左旋

上面如何去记呢,就是说p->parent= r一定是最后改的,这就是第六个指针,然后第一个指针就改p->right = pr->left(也就是说把分叉给连上呢),这个p->right(pr)这个时候就改了,对吗,因为后面还要用户p->right去她的left还有它的parent,所以我们这里必须要备份 。

下面代码实现

package com.pxx.tree.rbtree;

public class RBTree<K extends Comparable<K>, V> {
    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;
    }

    //左旋
    /***
     *  p
     * / \
     *pl  pr
     *   /  \
     *   rl  rr
     * @param p
     */
    private void leftRotate(RBNode p) {
        if (p != null) {
            //让p的右边指向r的左边
            RBNode r = p.right;//先把r这个结点备份出来,因为等会要改变
            p.right = r.left;
            //因为pr左边可能没有,有的话,还要让rl.parent指向p
            if (r.left != null) {
                r.left.parent = p;
            }
            //现在要改变根结点的父结点的
            r.parent = p.parent;
            //下面我们要去改变p之前parent的子结点
            //之前是p嘛,现在要改成pr
            //还要考虑的是p这个结点之前是不是根结点
            //这种情况如果是根结点
            if (p.parent == null) {
                root = r;//那么r上去就直接是根结点
            } else if (p.parent.left == p) { //p是左孩子,要改成r是左孩子
                p.parent.left = r;
            } else {
                p.parent.right = r;//那么p就是右孩子
            }

            //现在处理r的左边,也就是旋转之后左边子结点的处理
            r.left = p;
            p.parent = r;
        }
    }

   
    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(K key, V value, RBNode parent) {
            this.parent = parent;
            this.color = BLACK;
            this.key = key;
            this.value = value;
        }

        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() {
        }

        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() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }

}

 下面就是右旋,右旋就是把上面left变成right就可以了,因为左旋与右旋是不是就是反向操作呢

//右旋就是把上面左改成右,右改成左
    private void rightRotate(RBNode p) {
        if (p != null) {
            RBNode l = p.left;//右旋看左边
            p.left = l.right;//左边看右边,这个l.right可能等于null,没有关系
            if (l.right != null) {//看右边有没有值,有的话,我们它的父节点
                l.right.parent = p;//把右边放左边
            }
            l.parent = p.parent;//先把这个根父类搭上
            // 再去修改父类的指向
         if (p.parent == null) {
                root = l;
            } else if (p.parent.right == p) {
                p.parent.right = l;
            } else {
                p.parent.left = l;
            }
            l.right = p;//这里就搭右边
            p.parent = l;//最后改p.parent,前面要依靠他
        }
    }

下面讲解一下红黑树结点的插入

首先插入的叶子必须是红色结点

下面我们讲解2-3-4树插入情况与红黑树的对应关系

1.1个结点,不合并

2. 新增一个结点,与2结点合并,直接合并,变成了三结点

 这种红黑树对应的情况就是:新增一个红色结点+黑色接地那 = 上黑下红的形态(不调整)

3.新增了一个节点与三结点合并,就变成了四结点

红黑树:新增了一个红色结点 + 上黑下红 = 正确形态是中间是黑色,两边是红色

4.新增一个结点,与四结点进行合并,就要分裂,原来的的四节点中间升级为父节点,新增元素与剩下其中一个合并

红黑树呢就是:新增红色+爷爷黑色,父亲与叔叔是红色 = 爷爷变红(如果爷爷是黑色,变黑),父亲与叔叔变黑

然后整个红黑树插入,也是来依次比较值,然后在叶结点也就是合适的位置进行插入。其实整个插入的过程非常顺畅,难点在于修正的过程

下面根据上面关系,我们完成代码操作,代码内部有详细的说明注释,请仔细分析

package com.pxx.tree.rbtree;

public class RBTree<K extends Comparable<K>, V> {
    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;
    }

    //左旋
    /***
     *  p
     * / \
     *pl  pr
     *   /  \
     *   rl  rr
     * @param p
     */
    private void leftRotate(RBNode p) {
        if (p != null) {
            //让p的右边指向r的左边
            RBNode r = p.right;//先把r这个结点备份出来,因为等会要改变
            p.right = r.left;
            //因为pr左边可能没有,有的话,还要让rl.parent指向p
            if (r.left != null) {
                r.left.parent = p;
            }
            //现在要改变根结点的父结点的
            r.parent = p.parent;
            //下面我们要去改变p之前parent的子结点
            //之前是p嘛,现在要改成pr
            //还要考虑的是p这个结点之前是不是根结点
            //这种情况如果是根结点
            if (p.parent == null) {
                root = r;//那么r上去就直接是根结点
            } else if (p.parent.left == p) { //p是左孩子,要改成r是左孩子
                p.parent.left = r;
            } else {
                p.parent.right = r;//那么p就是右孩子
            }

            //现在处理r的左边,也就是旋转之后左边子结点的处理
            r.left = p;
            p.parent = r;
        }
    }

    //右旋就是把上面左改成右,右改成左
    private void rightRotate(RBNode p) {
        if (p != null) {
            RBNode l = p.left;//右旋看左边//备份左边
            p.left = l.right;//左边看右边,这个l.right可能等于null,没有关系
            if (l.right != null) {//看右边有没有值,有的话,我们它的父节点
                l.right.parent = p;//把右边放左边
            }
            l.parent = p.parent;//先把这个根父类搭上
            // 再去修改父类的指向
         if (p.parent == null) {
                root = l;
            } else if (p.parent.right == p) {
                p.parent.right = l;
            } else {
                p.parent.left = l;
            }
            l.right = p;//这里就搭右边
            p.parent = l;//最后改p.parent,前面要依靠他
        }
    }

    //获取父节点的方法
    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 boolean colorOf(RBNode node) {
        return node == null ? BLACK : node.color;//为空,那默认就是黑色
    }

    //设置颜色的方法
    private void setColor(RBNode node, boolean color) {
        if (node != null) {
            node.color = color;
        }
    }

    public void put(K key, V value) {
        RBNode t = this.root;//先拿到根结点
        if (t == null) {
            //这里不是RBNode<K,V>为了处理value为null情况。默认值为k,
            //但是这个key是K类型,要兼容只能用Object类型
            RBNode<K, Object> root= new RBNode<>(key, value == null ? key : value, null);
            return;
        }

        //下面我们结点进来就要找插入的位置
        //寻找插入位置
        RBNode parent;//保留t的父亲
        int cmp;//等会用来比较的值
        if (key == null) {
            throw new NullPointerException();
        }
        //沿着根节点寻找插入位置
        do {
            parent = t;
            //首先判断当前对象与传入对象大小,此时决定了往左边走还是往右边走
            //传入小,走左边,传入大,走右边
            //这里比较this - 传入者
            //> 0 表示传入的小-> right    < 0 表示传入的大 走left(注意传入的是当前根结点)
            cmp = key.compareTo((K)t.key);//调用k的实现的Comparable<k>比较器里面的方法,但是这里t.key它是一个
            //K extends Comparable<K>也就是Comparable,所以强制类型转换一下
            if (cmp < 0) {
                t = t.left;
            } else if (cmp > 0) {
                t = t.right;
            } else { //两个值相等的情况
                //相等的key,传入的value会覆盖掉原来的value
                t.setValue(value == null ? key : value);
                return;//这里找到了就要退出
            }
        } while (t != null);//会让t一直往后面找

        //上面跳出来之后,说明就是已经到了指定的位置上面
        //因为你始终必须知道插入是在叶子结点上面进行的
        //此时parent就是插入结点的父类,t讲就已经到了null的位置

        //先来在堆上新创建一个结点
        RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
        //下面我们要判断是parent的左子树还是右子树
        //最后跳出的cmp
        if (cmp  < 0) {
            //根大,走左边
            parent.left = e;
        } else {
            parent.right = e;
        }

        //开始修正(插入的这个e结点可能就是需要修正的)
        //也可能不需要修正,我们在内部进行讨论
        //其实只要情况4进来的时候才会走第一次的while循环,而一次递归就是走colorOf(y)判断爷爷是红色这条路
        fixAfterPut(e);
    }

    //开始进行修正
    private void fixAfterPut(RBNode x) {
        x.color = RED;//变红,默认结点是黑色(进来变红色)
        //本质上就是父节点是黑色就不需要调整,
        //新节点进来与1结点合并,变为2结点不需要调整
        //新节点进来与3结点合并,需要调整左左或者右右,左左与右右的操作其实就是相反的嘛
        //看上面的第三种情况,调整右右,左旋,上面变黑,下面变红  调整左左,就右旋,上面变黑,下面变红
        //还要指注意,我们调整,不能只调整一棵树的片段
        //需要调整整棵树的部分
        //所以下面必须用while循环进行一个递归(递归到根结点结束)
        while (x != null && x != root && x.parent.color == RED) {
            //1.调整情况3的左左,注意这个x是插入的结点
            //x的父节点是爷爷左孩子(左3)(这个就是情况3的左左)
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                //拿到叔叔,叔叔结点为红,那么爷爷变红,父亲变黑,叔叔变红,也就是情况四的四种情况
                //第四种情况直接调整为正确情况,并且要把x交给爷爷,继续往上面判断
                //然后如果在第四种把情况插入中出现第三种情况就走else的情况
                RBNode y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x),BLACK);//父亲变黑
                    setColor(y,BLACK);//叔叔变黑
                    setColor(parentOf(parentOf(x)), RED);//爷爷变红
                    //这种情况一棵局部树就会变成正确状态,但是我们要去看父类正不正确
                    x = parentOf(parentOf(x));//以爷爷为子结点往上继续判断
                    //不用在x里面进行递归了,因为每次的else,就会把叶子结点部分变为有序的状态,这个
                    //状态不用递归,然后下一次进来肯定走colorOf,然后继续往上到根结点
                } else {
                    //在第三种情况里面,还有一种左左是把不笔直的左左
                    //就是x结点可能在父亲的右边,可以对应情况3去看一下
                    //如果是情况3右右的情况,就要调整左边
                    if (x == rightOf(parentOf(x))) {
                        //其实就是要调整为笔直的左左
                        //根据x的parent进行左旋
                        x=parentOf(x);//利用父节点左旋
                        leftRotate(x);
                    }
                    //下面就是彻底的情况3的情况,父亲为红,插入为红,左左
                    //这种情况就是,下面的操作左左与右右都是一样的
                    //父亲变黑,爷爷变红,这是左左,考虑右旋(根据爷爷右旋)
                    setColor(parentOf(x), BLACK);//父亲变黑
                    setColor(parentOf(parentOf(x)), RED);//爷爷变红
                    //根据爷爷右旋,成为情况3里面正确的状态
                    rightRotate(parentOf(parentOf(x)));
                }
                //然后以
            } else {//左左就判断完了
                   //第三种情况的右右
                    //右右的话,叔叔就是爷爷结点的左边
                    RBNode y = leftOf(parentOf(parentOf(x)));//改成左边
                    if (colorOf(y) == RED) {
                        setColor(parentOf(x),BLACK);//父亲变黑
                        setColor(y,BLACK);//叔叔变黑
                        setColor(parentOf(parentOf(x)), RED);//爷爷变红
                        //这里也要变成爷爷为子结点往上走
                        x = parentOf(parentOf(x));
                    } else {
                        //在第三种情况里面,还有一种左左是把不笔直的左左
                        //就是x结点可能在父亲的右边,可以对应情况3去看一下
                        //如果是情况3右右的情况,就要调整左边
                        if (x == leftOf(parentOf(x))) {//右右变为左边
                            //其实就是要调整为笔直的左左
                            //根据x的parent进行左旋
                            x=parentOf(x);//利用父节点右旋
                            rightRotate(x);
                        }
                        //下面就是彻底的情况3的情况,父亲为红,插入为红,左左
                        //这种情况就是,下面的操作左左与右右都是一样的
                        //父亲变黑,爷爷变红,这是左左,考虑右旋(根据爷爷右旋)
                        setColor(parentOf(x), BLACK);//父亲变黑
                        setColor(parentOf(parentOf(x)), RED);//爷爷变红
                        //根据爷爷右旋,成为情况3里面正确的状态
                        leftRotate(parentOf(parentOf(x)));//右右就是左旋
                    }
            }//右右判断完毕
        }//整体while循环
        //最后结束了rooty一定要变黑
        root.color = BLACK;
    }

    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(K key, V value, RBNode parent) {
            this.parent = parent;
            this.color = BLACK;
            this.key = key;
            this.value = value;
        }

        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() {
        }

        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() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }


}

红黑树的删除操作 

这个可以用二叉搜索树的删除来看

1.删除叶子结点,直接删除

2.删除结点有一个子结点,那么就用子结点来替代

3.如果删除结点有两个子结点,那么就要找到直接前驱或者直接后继结点来替代(把这个替代结点的k与v放到删除上面去,然后把k与v都给放空)

上面不太懂的具体可以看我写的文章二叉搜索树的一个删除操作

下面我们先来写找到直接前驱或者找到直接后继的代码

先来看一张图

拿着这个图来进行思考啊,下面代码看不懂,就配合着这个图来看

/**
     * 找到指定结点的前驱结点,小于node结点的最大值
     * @param node
     * @return
     */
    private RBNode predecessor(RBNode node) {
        if (node == null) {
            return null;
        } else if (node.left != null) {//找到当前结点的左边,然后干到右边的尽头
            RBNode p = node.left;
            //直接干到右边
            while (p.right != null) {
                p = p.right;
            }
            //上面跳出了,直接返回p
            return p;
        } else {
            //这个情况找前驱,其实我们不会进来
            //因为这个相当于就是左边没有子结点,你怎么往下找前驱啊
            //这个结点也不用找前驱了,直接右边拉上来接上就行了或者直接删除(叶子结点)
            //这个情况也分为左右结点,左边的就要一直往上,直到right = p结束
            RBNode p = node.parent;
            //考虑为右叶子结点找前驱
            if (p != null && p.right == node) {
                return p;//直接就是p
            } else {
                //考虑为左叶子结点
                RBNode cur = node;
                while (p != null && cur == p.left) {//一旦p.right = cur,那么这个p就是前驱结点
                    cur = p;
                    p = p.parent;
                }
                return p;
            }
        }
    }

    //找后继结点
    /**
     * 找后继
     * @param node
     * @return
     */
    private RBNode successors(RBNode node) {
        //就是与上面完全相反的一种考虑方式
        //找右子树左子树的最大值
        if (node == null) {
            return null;
        } else if (node.right != null) {
            //拿到右子树的第一个左子树,然后干到右边的尽头
            RBNode p = node.right;
            while (p.left != null) {//左边
                p = p.left;
            }
            return p;
        } else {//其实这条线根本不会走进来,子在写的时候也可以忽略掉
            //考虑为叶子结点没有右子树,就一直往上直到parent.right != node
            //然后上面是考虑为右结点的情况
            //如果是左结点,直接就是parent
            RBNode p = node.parent;
            if (p!= null && p.left == node) {
                return p;
            } else {
                //考虑为右边叶结点
                RBNode cur = node;
                while (p != null && cur == p.right) {
                    p = p.parent;
                }
                return p;
            }
        }
    }

下面我们先把删除方法大体模板写上

 

 完成得到这个结点的方法

下面我们去完成删除方法的框架

 private void deleteNode(RBNode node) {
        //node结点有两个孩子(2个孩子都有)
        if (node.left != null && node.right != null) {
            //找到后继结点继续替换
            RBNode successor = successors(node);
            //把键和值都给替换掉
            node.key = successor.key;
            node.value = successor.value;
            //让node指向后继结点
            node = successor;
        }

        //替换结点,看一下替换左边上去,还是替换右边上去
        //计算两边都有,拿哪一个上去都是可以的
        RBNode replacement = (node.left != null) ? node.left : node.right;
        //先来这样判断一下
        //一个节点删除之后,整棵树直接为空
        if (replacement == null && node.parent == null) {
            root = null;
        } else if (replacement != null) {
            //表明这有后继结点,左边或者右边,拿上去就行,非叶子结点
            //那当前node结点的父亲 给 替换结点的父亲(我说了结点拿上去就可以了)
            replacement.parent = node.parent;
            //认了父亲之后,就要连儿子
            if (node.parent == null) {
                //这里就表示node为根结点
                root = replacement;//随便拿上去一个节点就是根结点
            } else if (node == node.parent.left) {//判断是从左连还是从右连
                node.parent.left = replacement;
            } else {
                node.parent.right = replacement;
            }

            //开始把node进行脱钩,让gc进行销毁
            //把左右都断掉
            node.left = node.right = node.parent = null;//把相关的链全部断掉

            //调整,删除的如果是黑色结点,必须调整
            //红色不需要调整
            if (node.color == BLACK) {
                //需要调整
                fixAfterRemove(replacement);
            }
        } else if (replacement == null) {
            //表明就是叶子结点,直接删除
            //叶子结点直接删除
            //但是我们需要注意的是叶子结点如果先删除的话,是没有替换结点可以用来替换的
            //所以,删除叶子结点我们先调整后删除
            //先调整(s删除红色不调整)
            if (node.color == BLACK && node.parent != null) {//等于null整棵树就没了

            //fixAfterRemove(node)
            }
            //再删除
            //不是根结点
            if (node.parent != null) {//如果node是根接地那,左右为null,直接删了就是空树
                //不然如果走了找后继结点的话,node.parent不可能会为空
                if (node == node.parent.left) {
                    node.parent.left = null;//毕竟就是叶子结点
                } else {
                    node.parent.right = null;
                }
            } else {
                root = null;//整棵树直接挂掉
            }

            //把当前叶子结点给脱钩
            node.parent = null;
        }
    }

下面讲解一下删除的调整分析 

整体删除的位置 

下面说一下几种删除的情况

删除的情况1:自己能搞定,对应的是3结点与4结点

 

但是删除某个结点我们要注意变色的问题,也就是调整的问题,比如我们删除6结点,其实就是找到直接后继,也就是7,然后给替换上去,把7这个位置给删除掉,此时7是一个黑色结点,所以我们需要对树进行修正,这个时候7和7.5这一个节点,就只剩下7.5这个key ,而7.5是一个2结点,对应的红黑树关系,2结点直接变黑修正

那如果我们删除的是8,它的直接后继就是9,9是一个红色结点,红色结点删除之后就不需要修正

删除情况2:自己搞不定,需要跟兄弟借,但是兄弟不借,就借助父亲来借,父亲下来,然后兄弟找一个人替代父亲当家

 

 删除情况3:跟兄弟借,兄弟也没有

下面去写修正代码

关于兄弟结点的调整问题

我们从2-3-4树来分析的删除,删除5,我们要去判断兄弟结点能不能借,也就是 7和7.5能不能借的问题,这个情况只会发生在叶子结点,所以,当我们变成红黑树的时候
我们就要去拿到兄弟结点,然后看兄弟结点的哪一个结点是黑色,或者都是黑色,就表明没有结点
为什么是黑色,首先三结点的红黑树结构是上黑下红,也就是说,兄弟结点的孩子不管是在左边
还是在右边,都是红色。
话说回来,5对应的兄弟结点是7和7.5,那么看一下上面左边的红黑树,5对应的兄弟结点是8,很明显不对,所以我们需要如下调整

    1.先变色,8变成黑,他组成的三结点6变红,那么这里也就是说
    把8拿下去,6放下来,三结点是上黑下红嘛
    其实就是整体用8来进行左旋

我们看一下下面的图

变成真正的兄弟结点之前,是下面这张图,7结点的子结点略有调整但是不重要

 对应的代码如下

关于三结点如何借给兄弟结点key的问题 

 

 对应的代码

此时 

 

 那么这个时候rnode就已经变了

rnode = rightOf(parentOf(x));//还是父亲的右孩子调整一下指针

如果我们去找四节点来借

这个时候,5结点考虑为: 把6.5借上去到父亲,父亲把6借给5结点,那么这里还是考虑旋转,这里采用两个方案
第一个方案旋转两次

先把6.5与7还有7.5转成一条线 

再以6结点左旋,6.5转给父类,6转给子结点

 第二个方案旋转一次,我一下借两个结点我6,6.5结点一起借,我不止把父亲的借给你,还把我自己的一个结点分给你

这里是直接拿到6进行旋转,左旋

 

在旋转之前,我们要先注意改变颜色

在旋转之前,注意颜色的设置
5 6 6.5 是中间黑两边红,8是黑色,7上去二节点变红,7.5是2结点直接变黑色

代码

分析一下和兄弟借,但是兄弟也没有(这个时候兄弟情同手足,同时自损 

 

 

下面是完整代码

package com.pxx.tree.rbtree;

public class RBTree<K extends Comparable<K>, V> {
    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;
    }

    //左旋
    /***
     *  p
     * / \
     *pl  pr
     *   /  \
     *   rl  rr
     * @param p
     */
    private void leftRotate(RBNode p) {
        if (p != null) {
            //让p的右边指向r的左边
            RBNode r = p.right;//先把r这个结点备份出来,因为等会要改变
            p.right = r.left;
            //因为pr左边可能没有,有的话,还要让rl.parent指向p
            if (r.left != null) {
                r.left.parent = p;
            }
            //现在要改变根结点的父结点的
            r.parent = p.parent;
            //下面我们要去改变p之前parent的子结点
            //之前是p嘛,现在要改成pr
            //还要考虑的是p这个结点之前是不是根结点
            //这种情况如果是根结点
            if (p.parent == null) {
                root = r;//那么r上去就直接是根结点
            } else if (p.parent.left == p) { //p是左孩子,要改成r是左孩子
                p.parent.left = r;
            } else {
                p.parent.right = r;//那么p就是右孩子
            }

            //现在处理r的左边,也就是旋转之后左边子结点的处理
            r.left = p;
            p.parent = r;
        }
    }

    //右旋就是把上面左改成右,右改成左
    private void rightRotate(RBNode p) {
        if (p != null) {
            RBNode l = p.left;//右旋看左边//备份左边
            p.left = l.right;//左边看右边,这个l.right可能等于null,没有关系
            if (l.right != null) {//看右边有没有值,有的话,我们它的父节点
                l.right.parent = p;//把右边放左边
            }
            l.parent = p.parent;//先把这个根父类搭上
            // 再去修改父类的指向
         if (p.parent == null) {
                root = l;
            } else if (p.parent.right == p) {
                p.parent.right = l;
            } else {
                p.parent.left = l;
            }
            l.right = p;//这里就搭右边
            p.parent = l;//最后改p.parent,前面要依靠他
        }
    }


    /**
     * 找到指定结点的前驱结点,小于node结点的最大值
     * @param node
     * @return
     */
    private RBNode predecessor(RBNode node) {
        if (node == null) {
            return null;
        } else if (node.left != null) {//找到当前结点的左边,然后干到右边的尽头
            RBNode p = node.left;
            //直接干到右边
            while (p.right != null) {
                p = p.right;
            }
            //上面跳出了,直接返回p
            return p;
        } else {
            //这个情况找前驱,其实我们不会进来
            //因为这个相当于就是左边没有子结点,你怎么往下找前驱啊
            //这个结点也不用找前驱了,直接右边拉上来接上就行了或者直接删除(叶子结点)
            //这个情况也分为左右结点,左边的就要一直往上,直到right = p结束
            RBNode p = node.parent;
            //考虑为右叶子结点找前驱
            if (p != null && p.right == node) {
                return p;//直接就是p
            } else {
                //考虑为左叶子结点
                RBNode cur = node;
                while (p != null && cur == p.left) {//一旦p.right = cur,那么这个p就是前驱结点
                    cur = p;
                    p = p.parent;
                }
                return p;
            }
        }
    }

    //找后继结点
    /**
     * 找后继
     * @param node
     * @return
     */
    private RBNode successors(RBNode node) {
        //就是与上面完全相反的一种考虑方式
        //找右子树左子树的最大值
        if (node == null) {
            return null;
        } else if (node.right != null) {
            //拿到右子树的第一个左子树,然后干到右边的尽头
            RBNode p = node.right;
            while (p.left != null) {//左边
                p = p.left;
            }
            return p;
        } else {//其实这条线根本不会走进来,子在写的时候也可以忽略掉
            //考虑为叶子结点没有右子树,就一直往上直到parent.right != node
            //然后上面是考虑为右结点的情况
            //如果是左结点,直接就是parent
            RBNode p = node.parent;
            if (p!= null && p.left == node) {
                return p;
            } else {
                //考虑为右边叶结点
                RBNode cur = node;
                while (p != null && cur == p.right) {
                    p = p.parent;
                }
                return p;
            }
        }
    }

    //获取父节点的方法
    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 boolean colorOf(RBNode node) {
        return node == null ? BLACK : node.color;//为空,那默认就是黑色
    }

    //设置颜色的方法
    private void setColor(RBNode node, boolean color) {
        if (node != null) {
            node.color = color;
        }
    }

    public void put(K key, V value) {
        RBNode t = this.root;//先拿到根结点
        if (t == null) {
            //这里不是RBNode<K,V>为了处理value为null情况。默认值为k,
            //但是这个key是K类型,要兼容只能用Object类型
            RBNode<K, Object> root= new RBNode<>(key, value == null ? key : value, null);
            return;
        }

        //下面我们结点进来就要找插入的位置
        //寻找插入位置
        RBNode parent;//保留t的父亲
        int cmp;//等会用来比较的值
        if (key == null) {
            throw new NullPointerException();
        }
        //沿着根节点寻找插入位置
        do {
            parent = t;
            //首先判断当前对象与传入对象大小,此时决定了往左边走还是往右边走
            //传入小,走左边,传入大,走右边
            //这里比较this - 传入者
            //> 0 表示传入的小-> right    < 0 表示传入的大 走left(注意传入的是当前根结点)
            cmp = key.compareTo((K)t.key);//调用k的实现的Comparable<k>比较器里面的方法,但是这里t.key它是一个
            //K extends Comparable<K>也就是Comparable,所以强制类型转换一下
            if (cmp < 0) {
                t = t.left;
            } else if (cmp > 0) {
                t = t.right;
            } else { //两个值相等的情况
                //相等的key,传入的value会覆盖掉原来的value
                t.setValue(value == null ? key : value);
                return;//这里找到了就要退出
            }
        } while (t != null);//会让t一直往后面找

        //上面跳出来之后,说明就是已经到了指定的位置上面
        //因为你始终必须知道插入是在叶子结点上面进行的
        //此时parent就是插入结点的父类,t讲就已经到了null的位置

        //先来在堆上新创建一个结点
        RBNode<K, Object> e = new RBNode<>(key, value == null ? key : value, parent);
        //下面我们要判断是parent的左子树还是右子树
        //最后跳出的cmp
        if (cmp  < 0) {
            //根大,走左边
            parent.left = e;
        } else {
            parent.right = e;
        }

        //开始修正(插入的这个e结点可能就是需要修正的)
        //也可能不需要修正,我们在内部进行讨论
        //其实只要情况4进来的时候才会走第一次的while循环,而一次递归就是走colorOf(y)判断爷爷是红色这条路
        fixAfterPut(e);
    }

    //开始进行修正
    private void fixAfterPut(RBNode x) {
        x.color = RED;//变红,默认结点是黑色(进来变红色)
        //本质上就是父节点是黑色就不需要调整,
        //新节点进来与1结点合并,变为2结点不需要调整
        //新节点进来与3结点合并,需要调整左左或者右右,左左与右右的操作其实就是相反的嘛
        //看上面的第三种情况,调整右右,左旋,上面变黑,下面变红  调整左左,就右旋,上面变黑,下面变红
        //还要指注意,我们调整,不能只调整一棵树的片段
        //需要调整整棵树的部分
        //所以下面必须用while循环进行一个递归(递归到根结点结束)
        while (x != null && x != root && x.parent.color == RED) {
            //1.调整情况3的左左,注意这个x是插入的结点
            //x的父节点是爷爷左孩子(左3)(这个就是情况3的左左)
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                //拿到叔叔,叔叔结点为红,那么爷爷变红,父亲变黑,叔叔变红,也就是情况四的四种情况
                //第四种情况直接调整为正确情况,并且要把x交给爷爷,继续往上面判断
                //然后如果在第四种把情况插入中出现第三种情况就走else的情况
                RBNode y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x),BLACK);//父亲变黑
                    setColor(y,BLACK);//叔叔变黑
                    setColor(parentOf(parentOf(x)), RED);//爷爷变红
                    //这种情况一棵局部树就会变成正确状态,但是我们要去看父类正不正确
                    x = parentOf(parentOf(x));//以爷爷为子结点往上继续判断
                    //不用在x里面进行递归了,因为每次的else,就会把叶子结点部分变为有序的状态,这个
                    //状态不用递归,然后下一次进来肯定走colorOf,然后继续往上到根结点
                } else {
                    //在第三种情况里面,还有一种左左是把不笔直的左左
                    //就是x结点可能在父亲的右边,可以对应情况3去看一下
                    //如果是情况3右右的情况,就要调整左边
                    if (x == rightOf(parentOf(x))) {
                        //其实就是要调整为笔直的左左
                        //根据x的parent进行左旋
                        x=parentOf(x);//利用父节点左旋
                        leftRotate(x);
                    }
                    //下面就是彻底的情况3的情况,父亲为红,插入为红,左左
                    //这种情况就是,下面的操作左左与右右都是一样的
                    //父亲变黑,爷爷变红,这是左左,考虑右旋(根据爷爷右旋)
                    setColor(parentOf(x), BLACK);//父亲变黑
                    setColor(parentOf(parentOf(x)), RED);//爷爷变红
                    //根据爷爷右旋,成为情况3里面正确的状态
                    rightRotate(parentOf(parentOf(x)));
                }
                //然后以
            } else {//左左就判断完了
                   //第三种情况的右右
                    //右右的话,叔叔就是爷爷结点的左边
                    RBNode y = leftOf(parentOf(parentOf(x)));//改成左边
                    if (colorOf(y) == RED) {
                        setColor(parentOf(x),BLACK);//父亲变黑
                        setColor(y,BLACK);//叔叔变黑
                        setColor(parentOf(parentOf(x)), RED);//爷爷变红
                        //这里也要变成爷爷为子结点往上走
                        x = parentOf(parentOf(x));
                    } else {
                        //在第三种情况里面,还有一种左左是把不笔直的左左
                        //就是x结点可能在父亲的右边,可以对应情况3去看一下
                        //如果是情况3右右的情况,就要调整左边
                        if (x == leftOf(parentOf(x))) {//右右变为左边
                            //其实就是要调整为笔直的左左
                            //根据x的parent进行左旋
                            x=parentOf(x);//利用父节点右旋
                            rightRotate(x);
                        }
                        //下面就是彻底的情况3的情况,父亲为红,插入为红,左左
                        //这种情况就是,下面的操作左左与右右都是一样的
                        //父亲变黑,爷爷变红,这是左左,考虑右旋(根据爷爷右旋)
                        setColor(parentOf(x), BLACK);//父亲变黑
                        setColor(parentOf(parentOf(x)), RED);//爷爷变红
                        //根据爷爷右旋,成为情况3里面正确的状态
                        leftRotate(parentOf(parentOf(x)));//右右就是左旋
                    }
            }//右右判断完毕
        }//整体while循环
        //最后结束了rooty一定要变黑
        root.color = BLACK;
    }


    //删除操作(返回删除的value值)
    public V remove(K key) {
        //要删除值先拿到值
        RBNode node = getNode(key);
        if (node == null) {
            return null;
        }
        V oldVlaue = (V)node.getValue();
        //删除
        deleteNode(node);
        return oldVlaue;
    }

    private void deleteNode(RBNode node) {
        //node结点有两个孩子(2个孩子都有)
        if (node.left != null && node.right != null) {
            //找到后继结点继续替换
            RBNode successor = successors(node);
            //把键和值都给替换掉
            node.key = successor.key;
            node.value = successor.value;
            //让node指向后继结点
            node = successor;
        }

        //替换结点,看一下替换左边上去,还是替换右边上去
        //计算两边都有,拿哪一个上去都是可以的
        RBNode replacement = (node.left != null) ? node.left : node.right;
        //先来这样判断一下
        //一个节点删除之后,整棵树直接为空
        if (replacement == null && node.parent == null) {
            root = null;
        } else if (replacement != null) {
            //表明这有后继结点,左边或者右边,拿上去就行,非叶子结点
            //那当前node结点的父亲 给 替换结点的父亲(我说了结点拿上去就可以了)
            replacement.parent = node.parent;
            //认了父亲之后,就要连儿子
            if (node.parent == null) {
                //这里就表示node为根结点
                root = replacement;//随便拿上去一个节点就是根结点
            } else if (node == node.parent.left) {//判断是从左连还是从右连
                node.parent.left = replacement;
            } else {
                node.parent.right = replacement;
            }

            //开始把node进行脱钩,让gc进行销毁
            //把左右都断掉
            node.left = node.right = node.parent = null;//把相关的链全部断掉

            //调整,删除的如果是黑色结点,必须调整
            //红色不需要调整
            if (node.color == BLACK) {
                //需要调整,替换结点一定是红色这里
                fixAfterRemove(replacement);
            }
        } else if (replacement == null) {
            //表明就是叶子结点,直接删除
            //叶子结点直接删除
            //但是我们需要注意的是叶子结点如果先删除的话,是没有替换结点可以用来替换的
            //所以,删除叶子结点我们先调整后删除
            //先调整(s删除红色不调整)
            if (node.color == BLACK && node.parent != null) {//等于null整棵树就没了

            //fixAfterRemove(node)
            }
            //再删除
            //不是根结点
            if (node.parent != null) {//如果node是根接地那,左右为null,直接删了就是空树
                //不然如果走了找后继结点的话,node.parent不可能会为空
                if (node == node.parent.left) {
                    node.parent.left = null;//毕竟就是叶子结点
                } else {
                    node.parent.right = null;
                }
            } else {
                root = null;//整棵树直接挂掉
            }

            //把当前叶子结点给脱钩
            node.parent = null;
        }
    }

    //开始写删除调整的代码:
    private void fixAfterRemove(RBNode x) {
        //你想一下,自己搞不定的话
        //那么也就是说只有一个节点
        //也就是说,它是2结点的情况是自己搞不定的,删除红色,不调整
        //所以这个结点进来调整肯定是黑色
        //那么这里就包含了情况2与情况3
        while (x != root && colorOf(x) == BLACK) {
            //不管是有得借还是没得借,都要先去找兄弟结点(有借没借都通过2-3-4树考虑)
            //兄弟结点我们又要看是左边的兄弟结点还是右边的兄弟结点
            if (x == leftOf(parentOf(x))) {
                //这里就是x是左孩子的情况
                //这是左孩子,就找右孩子
                RBNode rnode = rightOf(parentOf(x));

                //判断此时兄弟结点是不是真正的兄弟结点
                //比如5与8,这里等会用图说明
                //红色的情况,就不是真正的兄弟结点
                //把rnode变黑,6变红
                if (colorOf(rnode) == RED) {
                    setColor(rnode,BLACK);
                    setColor(parentOf(x), RED);//6变红
                    //沿着6开始左旋
                    leftRotate(parentOf(x));//就会变成正确的兄弟结点
                    //重新把兄弟结点变一下,真正的兄弟,这里是7
                    rnode = rightOf(parentOf(x));//旋转之后,x的父亲的右孩子就是我们的兄弟
                }


                //情况三,找兄弟借,兄弟没得借
                if (colorOf(leftOf(rnode)) == BLACK && colorOf(rightOf(rnode)) == BLACK) {
                    //把兄弟变成红色,自损
                    setColor(rnode, RED);
                    //然后递归自己父类,父类只要是红色,就变黑就平衡了
                    x = parentOf(x);
                } else {
                    //这里就是情况2,找兄弟借,兄弟有得借
                    //里面借的话又分成两种小情况:1.兄弟结点本来是3结点借或者4结点借的情况

                    //第一种是三结点的情况(比如7和7.5),这个结点只会连一个孩子的情况左为空或者右为空
                    //这个都是在叶子结点来考虑,三结点,就是上黑下红,如果左边或者右边为黑,则表示为空
                    if (colorOf(rightOf(rnode)) == BLACK) {//6.5左边 7在上面,三结点的情况
                        //兄弟结点的左孩子,也就是6.5
                        setColor(leftOf(rnode), BLACK);
                        //rnode变红
                        setColor(rnode,RED);
                        //然后把6.5和7进行右旋,目的是为了给到父亲
                        //父亲在借给它的左孩子
                        rightRotate(rnode);
                        rnode = rightOf(parentOf(x));//还是父亲的右孩子
                    }

                    //上面是三结点才进去,四结点直接走下面
                    //把7变红,她要去上去与8黑成为二节点(rnode变红)
                    setColor(rnode,colorOf(parentOf(x)));//其实就是把7变成父亲的颜色
                    //这里父亲的颜色不一定是红色,也可能是黑色
                    //在把父亲的颜色变黑,也就是6颜色,两边是红色5和6.5
                    setColor(parentOf(x), BLACK);
                    //下面要把兄弟的右孩子变成黑色,毕竟借出去了之后,它是一个二节点,直接变黑
                    setColor(rightOf(rnode), BLACK);
                    //5是要删除的所以不用管
                    //上面变完颜色之后,沿着6左旋,也就是x的父亲
                    leftRotate(parentOf(x));
                    //注意最后让x指向root
                    x= root;//表明这次判定结束
                }
            } else {
                //左右相反,左改右,右改左
                //这里就是x是左孩子的情况
                //这是左孩子,就找右孩子
                RBNode lnode = leftOf(parentOf(x));

                //判断此时兄弟结点是不是真正的兄弟结点
                //比如5与8,这里等会用图说明
                //红色的情况,就不是真正的兄弟结点
                //把rnode变黑,6变红
                if (colorOf(lnode) == RED) {
                    setColor(lnode,BLACK);
                    setColor(parentOf(x), RED);//6变红
                    //沿着6开始左旋
                    rightRotate(parentOf(x));//就会变成正确的兄弟结点
                    //重新把兄弟结点变一下,真正的兄弟,这里是7
                    lnode = leftOf(parentOf(x));//旋转之后,x的父亲的右孩子就是我们的兄弟
                }


                //情况三,找兄弟借,兄弟没得借
                if (colorOf(leftOf(lnode)) == BLACK && colorOf(rightOf(lnode)) == BLACK) {
                    //把兄弟变成红色,自损
                    setColor(lnode, RED);
                    //然后递归自己父类,父类只要是红色,就变黑就平衡了
                    x = parentOf(x);
                } else {
                    //这里就是情况2,找兄弟借,兄弟有得借
                    //里面借的话又分成两种小情况:1.兄弟结点本来是3结点借或者4结点借的情况

                    //第一种是三结点的情况(比如7和7.5),这个结点只会连一个孩子的情况左为空或者右为空
                    //这个都是在叶子结点来考虑,三结点,就是上黑下红,如果左边或者右边为黑,则表示为空
                    if (colorOf(leftOf(lnode)) == BLACK) {//6.5左边 7在上面,三结点的情况
                        //兄弟结点的左孩子,也就是6.5
                        setColor(rightOf(lnode), BLACK);
                        //rnode变红
                        setColor(lnode,RED);
                        //然后把6.5和7进行右旋,目的是为了给到父亲
                        //父亲在借给它的左孩子
                        leftRotate(lnode);
                        lnode = leftOf(parentOf(x));//还是父亲的右孩子
                    }

                    //上面是三结点才进去,四结点直接走下面
                    //把7变红,她要去上去与8黑成为二节点(rnode变红)
                    setColor(lnode,colorOf(parentOf(x)));//其实就是把7变成父亲的颜色
                    //这里父亲的颜色不一定是红色,也可能是黑色
                    //在把父亲的颜色变黑,也就是6颜色,两边是红色5和6.5
                    setColor(parentOf(x), BLACK);
                    //下面要把兄弟的右孩子变成黑色,毕竟借出去了之后,它是一个二节点,直接变黑
                    setColor(leftOf(lnode), BLACK);
                    //5是要删除的所以不用管
                    //上面变完颜色之后,沿着6左旋,也就是x的父亲
                    rightRotate(parentOf(x));
                    //注意最后让x指向root
                    x= root;//表明这次判定结束
                }
            }
        }
        
        //情况1,自己能搞定
        setColor(x, BLACK);//提点结点是红色,直接染黑,补偿删除的黑色
    }

    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;
    }

    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(K key, V value, RBNode parent) {
            this.parent = parent;
            this.color = BLACK;
            this.key = key;
            this.value = value;
        }

        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() {
        }

        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() {
            return color;
        }

        public void setColor(boolean color) {
            this.color = color;
        }

        public K getKey() {
            return key;
        }

        public void setKey(K key) {
            this.key = key;
        }

        public V getValue() {
            return value;
        }

        public void setValue(V value) {
            this.value = value;
        }
    }


}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/161016.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

openfeign整合sentinel出现异常

版本兼容的解决办法&#xff1a;在为userClient注入feign的接口类型时&#xff0c;添加Lazy注解。 Lazy注解是Spring Framework中的一个注解&#xff0c;它通常用于标记Bean的延迟初始化。当一个Bean被标记为Lazy时&#xff0c;Spring容器在启动时不会立即初始化这个Bean&…

QUIC协议详解

前言协议特点QUIC协议与HTTP/2协议区别QUIC协议的多路复用技术优势QUIC协议在Java中的应用存在的问题 前言 QUIC&#xff08;Quick UDP Internet Connections&#xff09;是一种基于 UDP 的传输层协议&#xff0c;由 Google 提出。从协议栈可以看出&#xff0c;QUIC HTTP/2 …

git基本操作(配图超详细讲解)

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 创建git本地仓库 配置仓库 认识工作区&#xff0c;暂存区&#xff0c;版本库 修改文件 版本回退 撤销修改 删除文件 创建git本地仓库 要提前说的是&#xff0c;仓库是进⾏版本控制的⼀个⽂件⽬录。我们要想对⽂…

Python语言:随机生成几个数案例分析讲解

背景 在某一节课上&#xff0c;老师使用xx通软件随机生成几个学号&#xff0c;并让对应学号同学回答问题。虽然我对xx通软件没啥好感&#xff0c;不过感觉这个随机生成学号功能挺不错的。今天我用python实现一个这个功能。 其实原理挺简单的&#xff0c;就是就是在一堆数字里随…

伦敦银和美白银的关系

与黄金相似&#xff0c;世界上白银交易的基础就是伦敦白银市场&#xff0c;人们利用设立在伦敦的专们负责清算银行&#xff08;与黄金的清算银行相同&#xff09;所开设的账户进行白银保证金交易。在伦敦市场&#xff0c;以美元清算的伦敦白银价格&#xff0c;是以美元买进1金衡…

【广州华锐互动】VR技术助力中小学生安全教育,让学生在虚拟世界中学会自我保护!

随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术已经逐渐走进我们的生活。在教育领域&#xff0c;VR技术的应用也日益广泛&#xff0c;为传统的教育模式带来了革命性的变革。中小学生安全教育作为学生生活中的重要组成部分&#xff0c;其重要性不言而喻…

LeetCode47-全排列II-剪枝逻辑

参考链接: &#x1f517;:卡尔的代码随想录:全排列II 这里第一层,used只有一个元素为1,代表只取出了1个元素作为排列,第二层used有两个元素为1,代表取出了2个元素作为排列,因为数组有序,所以重复的元素都是挨着的,因此可以使用如下语句去重. 其中visit[i-1]False的话,就是代表…

深入理解Linux网络笔记(七):异常TCP连接建立情况、如何查看是否有连接队列溢出发生

本文为《深入理解Linux网络》学习笔记&#xff0c;使用的Linux源码版本是3.10&#xff0c;网卡驱动默认采用的都是Intel的igb网卡驱动 Linux源码在线阅读&#xff1a;https://elixir.bootlin.com/linux/v3.10/source 5、深度理解TCP连接建立过程&#xff08;二&#xff09; 4&…

为什么我学了几天 STM32 感觉一脸茫然?

为什么我学了几天 STM32 感觉一脸茫然&#xff1f; 刷到过b站的zhihui君吧&#xff0c;去看他的回答&#xff0c;他的第一块开发板是arduino&#xff0c;这种级别的人物&#xff0c;在国内也是大神级了&#xff0c;最早学电子方向也是用的arduino。最近很多小伙伴找我&#xff…

基于PLC的自动洗碗机控制系统(论文+源码)

1.系统设计 本课题基于PLC的自动洗碗机控制系统&#xff0c;在此将主要功能设定如下&#xff1a; 通过上下喷头旋转喷水湿润餐具&#xff1b;添加洗涤剂&#xff08;洗碗液&#xff09;&#xff1b;上下喷头喷水洗涤餐具&#xff1b;排出污水&#xff1b;往碗碟上喷洒更多的水…

适用于 Windows 的 10 款最佳数据恢复工具

在数字时代&#xff0c;数据就是一切。 从珍贵的家庭照片和重要的工作文件到我们最喜欢的音乐和电影&#xff0c;我们的生活存储在越来越多的不同设备上。 系统崩溃、意外删除或恶意病毒都可能导致有价值的数据在瞬间丢失。 这就是数据恢复工具的用武之地。 适用于 Windows 的 …

SpringMVC基础

文章目录 SpringMVC基础MVC理论基础第一个SpringMVC程序请求RequestMapping传递单参传递多参传递对象传递数组和集合传递JSON数据RequestParamPathVariableRequestPartCookie/Session获取Header 响应返回静态页面ResponseBody返回HTML代码⽚段返回JSON设置状态码设置Header设置…

Unity3D 解决相机拉高之后物体阴影消失

在Edit-Project Settings-Quality中找到Shadow Distance&#xff0c;将其调大即可

FreeRtos 任务切换深入分析

一、背景知识&#xff1a; 1、任务切换包含三个基本流程&#xff1a;保护现场、更新TCB、恢复现场并跳转 2、freertos的任务切换是在xPortPendSVHandler 中断函数中完成的 3、中断函数在调用之前&#xff0c;硬件已经保存了r0,r1,r2,r3,r12,r14(LR),r15(pc)&#xff0c;恢复…

山西电力市场日前价格预测【2023-11-18】

1.日前价格预测 预测说明&#xff1a; 如上图所示&#xff0c;预测明日&#xff08;2023-11-18&#xff09;山西电力市场全天平均日前电价为202.44元/MWh。其中&#xff0c;最高日前电价为346.71元/MWh&#xff0c;预计出现在18:00。最低日前电价为0.00元/MWh&#xff0c;预计…

Lesson 03 C/C++内存管理

C&#xff1a;渴望力量吗&#xff0c;少年&#xff1f; 文章目录 一、C内存管理方式1. new/delete操作内置类型2. new和delete操作自定义类型 二、operator new与operator delete函数三、new和delete的实现原理1. 内置类型2. 自定义类型 四、内存泄漏1. 什么是内存泄漏2. 内存泄…

基于Python实现大型家用电器和电子产品在线商店购买数据分析【500010098】

导入模块 import pandas as pd import numpy as np import matplotlib.pyplot as plt获取数据 df pd.read_csv( r"./data/kz.csv",sep,)数据描述 该数据包含2020年4月至2020年11月从大型家用电器和电子产品在线商店购买的数据。 数据说明 event_time&#xff1a…

[最新榜单] 智能手机数据恢复的 10 款最佳应用

当手机上的数据消失时&#xff0c;这让您感到非常难过。 由于事故而突然丢失重要的聊天记忆、照片和其他您想保留的东西的悲伤。 如果它没有被淹没&#xff0c;您可以使用数据恢复应用程序修复它。 在本文中&#xff0c;我们将解释一些有用的数据恢复应用程序。 数据恢复应用…

「Verilog学习笔记」数据选择器实现逻辑电路

专栏前言 本专栏的内容主要是记录本人学习Verilog过程中的一些知识点&#xff0c;刷题网站用的是牛客网 分析 将变量A、B接入4选1数据选择器选择输入端S0 S1。将变量C分配在数据输入端。从表中可以看出输出L与变量C的关系。 当AB00时选通D0而此时L0&#xff0c;所以数据端D0接0…

PyTorch 实战之水果分类

当我们试图提高神经网络的准确性时&#xff0c;经常会遇到过拟合训练数据的情况。当我们运行测试数据的模型时&#xff0c;这会导致一个糟糕的预测。因此&#xff0c;我采取了一个数据集&#xff0c;并应用这些技术&#xff0c;不仅提高准确性&#xff0c;而且还处理过拟合的问…