树之手撕红黑树

简单说一下二叉搜索树与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树的规则

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

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

那么如果另外一种情况是删了之后还是满足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树的在内存中的逻辑结构

总结一下:

插入:其实在叶上插,然后叶子满了,不够,向上分裂

这样插入的好处:
    这种插入方式的好处在于,每次插入都只涉及到一个结点,而不是整个树的重组。这降低了插入操作的复杂度,减少了IO操作的次数,从而提高了插入性能。
    B树的插入操作是从根结点逐级向下查找到叶子结点的过程。在这个过程中,只有包含插入关键字的路径上的结点需要被修改。由于磁盘IO往往是按块(block)进行的,这样的局部性保证了修改操作只涉及到有限数量的块,而不是整个树的结构。


删除:其实就是去借,借孩子,借父亲,子结点合并,父结点合并,终止保持这棵树是B树的状态,符合B树的规则

这样删除的好处:
    1.磁盘IO的优化
    B树的删除操作也是逐级向下进行的,只涉及到包含被删除关键字的路径上的结点。
    这减少了删除操作涉及的磁盘IO次数,从而提高了删除性能。

    2.平衡性
    B树的删除操作会保持树的平衡。删除关键字后,B树会根据一些规则进行调整,以保证树的高度相对较小,从而降低了查询操作的时间复杂度

    3.有序性
     B树的删除操作能够保持结点内关键字的有序性。即使在删除关键字后进行结点的合并等操作,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指向pif (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指向pif (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会覆盖掉原来的valuet.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的左子树还是右子树//最后跳出的cmpif (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;}//上面跳出了,直接返回preturn 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//然后上面是考虑为右结点的情况//如果是左结点,直接就是parentRBNode 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指向pif (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;}//上面跳出了,直接返回preturn 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//然后上面是考虑为右结点的情况//如果是左结点,直接就是parentRBNode 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会覆盖掉原来的valuet.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的左子树还是右子树//最后跳出的cmpif (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与情况3while (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));//就会变成正确的兄弟结点//重新把兄弟结点变一下,真正的兄弟,这里是7rnode = 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.5setColor(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.5setColor(parentOf(x), BLACK);//下面要把兄弟的右孩子变成黑色,毕竟借出去了之后,它是一个二节点,直接变黑setColor(rightOf(rnode), BLACK);//5是要删除的所以不用管//上面变完颜色之后,沿着6左旋,也就是x的父亲leftRotate(parentOf(x));//注意最后让x指向rootx= 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));//就会变成正确的兄弟结点//重新把兄弟结点变一下,真正的兄弟,这里是7lnode = 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.5setColor(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.5setColor(parentOf(x), BLACK);//下面要把兄弟的右孩子变成黑色,毕竟借出去了之后,它是一个二节点,直接变黑setColor(leftOf(lnode), BLACK);//5是要删除的所以不用管//上面变完颜色之后,沿着6左旋,也就是x的父亲rightRotate(parentOf(x));//注意最后让x指向rootx= 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;}}}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

大数据HCIE成神之路之数学(2)——线性代数

线性代数 1.1 线性代数内容介绍1.1.1 线性代数介绍1.1.2 代码实现介绍 1.2 线性代数实现1.2.1 reshape运算1.2.2 转置实现1.2.3 矩阵乘法实现1.2.4 矩阵对应运算1.2.5 逆矩阵实现1.2.6 特征值与特征向量1.2.7 求行列式1.2.8 奇异值分解实现1.2.9 线性方程组求解 1.1 线性代数内…

数据库课后习题加真题

文章目录 第二章第三章第四到六章某年真题 第二章 第三章 3.8 对于教学数据库的三个基本表&#xff1a; s( 学号 ‾ \underline{学号} 学号​&#xff0c;姓名&#xff0c;年龄, 性别) sc( 学号 , 课程号 ‾ \underline{学号, 课程号} 学号,课程号​, 成绩) c( 课程号 ‾ \un…

【Linux网络】详解使用http和ftp搭建yum仓库,以及yum网络源优化

目录 一、回顾yum的原理 1.1yum简介 yum安装的底层原理&#xff1a; yum的好处&#xff1a; 二、学习yum的配置文件及命令 1、yum的配置文件 2、yum的相关命令详解 3、yum的命令相关案例 三、搭建yum仓库的方式 1、本地yum仓库建立 2、通过http搭建内网的yum仓库 3、…

11 月 18 日 ROS 学习笔记——可视化和调试工具

文章目录 前言一、调试 ROS 节点1. gdb 调试器2. 在 ROS 节点启动时调用 gdb 调试器3. 在 ROS 节点启动时调用 valgrind 分析节点4. 设置 ROS 节点 core 文件转储5. 日志消息1). 输出日志消息2). 设置调试消息级别 二、检测系统状态1. rqt_graph2. 可视化坐标变换3. 保存与回放…

Leetcode—2760.最长奇偶子数组【简单】

2023每日刷题&#xff08;三十一&#xff09; Leetcode—2760.最长奇偶子数组 实现代码 #define MAX(a, b) ((a > b) ? (a): (b)) int longestAlternatingSubarray(int* nums, int numsSize, int threshold){int ans 0;int i 0;while(i < numsSize) {if(nums[i] >…

HUAWEI华为MateBook X 2020款i5集显(EUL-W19P)原装出厂Windows10系统

链接&#xff1a;https://pan.baidu.com/s/1eZuLjarWH2PjAWVqMWnzjQ?pwd2374 提取码&#xff1a;2374 原厂系统自带所有驱动、出厂主题壁纸、系统属性专属LOGO标志、Office办公软件、华为电脑管家等预装程序

迭代新品 | 第四代可燃气体监测仪,守护燃气管网安全快人一步

城市地下市政基础设施是城市有序运行的生命线&#xff0c;事关城市安全、健康运行和高质量发展。近年来&#xff0c;我国燃气事故多发、频发。2020、2021、2022 年分别发生燃气事故668、1140 起、802 起&#xff0c;造成92、106、66 人死亡&#xff0c;560、763、487 人受伤。尤…

【docker】iptables实现NAT

iptables是一个Linux内核中的防火墙工具&#xff0c;可以被用来执行各种网络相关的任务&#xff0c;如过滤、NAT和端口转发等&#xff0c;可以监控、过滤和重定向网络流量。 iptables可以用于以下应用场景&#xff1a; 网络安全&#xff1a;iptables可以过滤网络流量&#xf…

CMSIS-RTOS在stm32使用

目录&#xff1a; 一、安装和配置CMSIS_RTOS.1.打开KEIL工程&#xff0c;点击MANAGE RUN-TIME Environment图标。2.勾选CMSIS CORE和RTX.3.配置RTOS 时钟频率、任务栈大小和数量&#xff0c; 软件定时器. 二、CMSIS_RTOS内核启动和创建线程。1.包含头文件。2.内核初始化和启动。…

注解【元数据,自定义注解等概念详解】(超简单的好吧)

注解的理解与使用 注解的释义元数据的含义基础阶段常见的注解注解的作用&#xff08;包括但不限于&#xff09;教你读懂注解内部代码内容五种元注解尝试解读简单注解我当时的疑惑点 自定义注解自定义注解举例 注解的原理总结 注解的释义 我们都知道注释是拿来给程序员看的&…

SQL零基础入门教程,贼拉详细!贼拉简单! 速通数据库期末考!(七)

LEFT JOIN LEFT JOIN 同样用于关联两个表&#xff0c;ON 关键字后指定两个表共有的字段作为匹配条件&#xff0c;与 INNER JOIN 不同的地方在于匹配不上的数据行&#xff0c;INNER JOIN 对两表匹配不上的数据行不返回结果&#xff0c;而 LEFT JOIN 只对右表&#xff08;table2…

<C++>类和对象下|初始化列表|explicit static|友元|内部类|匿名对象|构造函数的优化

文章目录 1. 初始化列表2. explicit关键字3. 友元3.1 友元函数3.2 友元类 4. static关键字4.1 概念4.2 特性 5.内部类5.1 概念5.2 特性 6. 匿名对象7. 拷贝构造时的优化 1. 初始化列表 在类的构造函数体中&#xff0c;对成员属性写的操作叫做赋值&#xff0c;那么成员的初始化…