Featured image of post 一篇文章带你了解红黑树原理

一篇文章带你了解红黑树原理

前言

经过最近陆陆续续一周多的针对红黑树的学习,我对红黑树已经有了比较深刻的认识。接下来我将以我浅薄的理解,通过Java 8 TreeMap源码的分析带大家深入了解红黑树的插入、删除等算法。

本文的成文基于:

算法 第四版 第三章 平衡查找树

30张图带你彻底理解红黑树 - 简书

终结B站没人能讲清楚红黑树的历史 - 哔哩哔哩

阅读本文的前提条件需要对基本的树结构有所了解,且对 Java SE 编码基础有一定了解。

本文的下述代码默认您对于树的节点、叶子节点、(先序|中序|后序)遍历等基本知识已经了解。

若您对于BST已经有所了解可以跳过BST章节,直接从2-3-4树章节开始。但请绝对不要跳过2-3-4树直接看红黑树,这很重要。

BST

定义

BST也就是所谓的二叉查找树,下文简称BST。

对于BST的普遍定义为:在一颗二叉树的节点中其左子节点的值为空或比本节点小,其右子节点的值为空或比本节点大。

识别BST的一个简单的方案是按照中序遍历这个树得到的节点顺序是否是有序。

此外还有另一种在图中更容易表达中序遍历的方案,将节点压平在一条横向的轴上,在此轴上的从左至右顺序即为中序遍历的顺序。

图1-1 判别二叉查找树

节点的定义

在做BST的基本操作前,需要先来定义树结构的节点。首先需要明确节点的状态,也就是其属性。

  • 数据
  • 左子节点的引用
  • 右子节点的引用
  • 父节点的引用

数据很好理解,既然要实现数据结构那么不存储数据,又怎么叫数据结构。这里把数据分为两部分:用于比较和查找的Key和用于真实存储内容的Value。这种设计也正是 Java 映射(map)的方案。

左/右子节点的引用,是一个其他的节点对象用于存储左子或右子节点的内容。

父节点的引用,其内容为当前节点的父节点或者为空(当前节点是根节点),父节点的引用可以不设置,也能实现BST或红黑树。但不设置父节点会增加操作的复杂性。BST或红黑树的操作复杂性很高,多设置一个父节点对于减少不必要的逻辑,有很大的帮助。

我们可以将Key和Value类型都设计为Object类型,以便于它们可以存储任意类型的值。Key必须拥有比较大小的功能,那么可以将其设计为Comparable类型。这样设计的缺点是会产生很多的强制转换。在 Java 5 之后提供了泛型类型用于解决此类问题,我们的节点类也将沿用这种方案。(对于泛型类型了解不深的可以参考《Java 核心技术卷一 》第八章)。

以下将展示Entry(节点)类的简易代码(忽略了Get/Set、ToString、HashCode、Equals等方法)

完整代码点击此处跳转到底部代码清单1-1查看

1
2
3
4
5
6
7
public class Entry<K extends Comparable<K>, V> {
    private K key; // Key 值
    private V val; // Value 值
    private Entry<K, V> left; // 左子节点
    private Entry<K, V> right; // 右子节点
    private Entry<K, V> parent; // 父节点
}

<K extends Comparable<K>, V>,表示K类必须为实现Comparable<K>接口。K必须重写compareTo方法,我们也正是通过这个方法来对K值进行比较

BST插入方法的实现

BST的插入节点思想很简单,我们先将当前节点置为根节点

  • 若插入节点比当前节点大,就将当前节点置为当前节点的右子节点
  • 若插入节点比当前节点小,就将当前节点值位当前节点的左子节点

重复上述两部操作直到当前节点为空,那么此时当前节点的位置就是要插入新节点的位置

图2-2展示了H节点插入到一个BST中去的情景,其中红色的节点代表当前节点

图1-2 BST插入

此处我仅对寻找插入位置和插入节点的代码进行分析

BST插入的完整代码点击此处跳转到底部代码清单1-2查看

首先是寻找插入位置的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var t = this.root; // t 先置为根节点
int cmp; // 用于表示 两节点大小关系
Entry<K, V> parent;
do {
    parent = t; // 保存父节点
    cmp = key.compareTo(t.key); // 得到位置在当前节点的左或右子树
    if (cmp < 0) // 在左子树
        t = t.left; // 当前节点置为左子树
    else if (cmp > 0) // 在右子树
        t = t.right; // 当前节点置为右子树
    else { // 当前节点和插入节点相等
        t.val = val; // 覆盖当前节点的值
        return; // 直接退出方法
    }
} while (t != null); // 当前节点为空循环结束

在上述寻早查找位置的代码过后,parent的引用必然指向待插入节点的父节点,且cmp标示了插入节点为父节点的左子还是右子节点

接下来就是真正插入节点的过程

1
2
3
var e = new Entry<>(key, val, null, null, parent); // 创建一个新节点其父节点为parent
if (cmp < 0) parent.left = e; // 插入到左子节点
else parent.right = e; // 插入到右子节点

BST删除方法的实现

BST的删除比插入略复杂一点,分有三种情况

  • 若删除节点没有子节点,可以直接删除
  • 若删除节点有单个子节点,删除后用单个子节点替代删除的节点
  • 若删除节点有两个子节点,用删除节点的前驱节点或后继节点替换删除节点,然后就会转化为前两种情况

第三种情况可以通过特殊手段转化为前两种情况

图1-3 BST删除情况的转换

所谓的特殊手段即为用前驱或后继节点替换删除节点,然后将替换节点作为删除节点即可转化为前两种情况。

而对于前驱节点的普遍定义为,小于本节点的最大节点。后继节点为,大于本节点的最小节点。

看定义是比较抽象的,这里将图1-1拿过来。例如D节点的前驱节点为C而后继节点为E。

图1-1 判别二叉查找树

可以惊奇的发现将D的值替换为前驱C或后继E后将替换的节点删除,并不会影响BST的有序性。我们成功将有两个子节点的情况转化为了没有子节点的情况。

图1-4 BST删除前驱后继

当然还有一种情况就会转化为有一个子节点的情况,例如将再上图的C节点删除,其前继节点就为B,就有一个子节点。

此时有可能会有人提出问题,到底是使用前驱节点代替还是用后继节点代替呢?答案是都可以,且它们的效率基本相同。由于TreeMap的源码使用的是后继节点,故而本文的代码也使用后继节点作为代替节点。

下面让我们先实现查找后继节点的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private Entry<K, V> successor(Entry<K, V> h) {
    if (h == null) return null; // 当前节点为空当然后继节点也为空
    else if (h.right != null) { // 右子节点不为空
        var p = h.right;
        while (p.left != null) // 一直往右找 直到右边为空
            p = p.left;
        return p; // 返回后继节点
    } else { // 当前节点的右子节点为空
        var p = h.parent; // 定义节点存储父节点
        var ch = h; // 存储当前节点
        while (p != null && ch != rightOf(p)) { // 判断当前节点是否位父节点的右子节点
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

我们重点分析一下else中的右子节点为空的情况,首先此种情况不会发生,因为在对节点调用查找后继的时候必然是其有两个子节点的情况,所以右子节点不可能为空。但这仍然是找后继节点的一部分,源码中也给出了后面的实现,这里也就分析一下else里面的代码。

图1-5 后继节点特殊情况

当右子节点为空时,需要向上进行查找。若当前节点为其父节点的左子节点那么这个父节点就是要找的后继节点

接下来我们就着手实现删除方法,第一步骤还是比较简单的,我们先查找到待删除节点的位置,对于这一步我们先提取一个方法用来查找一个节点位置。

这个方法还是比较简单的,如果理解了前面的插入这里的查找基本不会出现理解问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private Entry<K, V> getNode(K key) {
    if (key == null) // 查找null直接抛出异常
        throw new NullPointerException();
    var node = root; // 从根节点开始查找
    while (node != null) { // 循环条件位当前节点不为空
        int cmp = key.compareTo(node.key);
        if (cmp < 0) // 查找节点比当前节点小,当前节点置为其左子节点
            node = node.left;
        else if (cmp > 0)
            node = node.right; // 查找节点比当前节点大,当前节点置为其右子节点
        else return node; // 相等就返回本节点
    }
    return null;
}

接着我们就根据之前的3种情况来实现删除的主要逻辑,这里还是将删除抽取称为一个方法

完整的删除代码点击此处跳转到底部代码清单1-3查看

因为第三种情况,可以分解为第一种或者第二种情况,这里代码使用一种精巧的方案来分解第三种情况

1
2
3
4
5
6
7
8
if (node.left != null && node.right != null) { // 子节点均为空,第三种情况
    var successor = successor(node); // 获得后继节点
    node.key = successor.key; // 替换当前节点的值为后继节点
    node.val = successor.val;
    node = successor; // 将当前节点指向为后继节点
}
// 若当前节点的左子树不为空就将替换节点置为左子节点,反之则置为右子节点(注意这里右子节点也可能为空那么替换节点就为空)
var replacement = node.left != null ? node.left : node.right;

上述的代码其实是对三种情况的普遍性分解,若为第三种情况就分解为第一或第二种情况。

最后一句代码就是区分第一种或第二种情况的代码,若replacement为空,那么就是第一种情况,不为空就为第二种情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (replacement != null) { // 不为空,必为第2种情况
    // 下述逻辑其实就是将当前节点替换为其子节点的过程
    replacement.parent = node.parent;
    // 若当前节点的父节点为空,那么当前节点就是根节点,根节点只有一个子节点,删除根节点,让子节点作为根节点即可
    if (node.parent == null) 
        root = replacement;
    else if (node == node.parent.left)
        node.parent.left = replacement;
    else node.parent.right = replacement;
    // 置空当前节点
    node.left = node.right = node.parent = null;
} else if(node.parent == null) { 
    // 替换节点为空的情况下,也就是第三种情况。若父节点为空,那么当前节点就是根节点,也就是仅有一个节点为父节点,直接删除即可
    root = null;
} else {
    // 最后就是当前节点不为根节点,且没有子节点的删除
    if (node.parent != null) {
        if (node == node.parent.left)
            node.parent.left = null;
        else if (node == node.parent.right)
            node.parent.right = null;
        node.parent = null;
    }
}

局限性

请思考一个问题,运用我们上述介绍的BST插入算法,插入顺序的一个序列例如A、B、C、D、E、F、G。

图1-6 后继节点特殊情况

具体结果应为上图,可以明显看到二叉树已经退化成一个链表了,其效率大幅度下降。对于处理这个问题就需要提出平衡二叉树了。

2-3-4树

2-3-4树介绍

首先2-3-4树是一种多路查找的平衡查找树。

所谓的平衡查找树即每个叶子节点到达根节点的高度是相同的,这样也就保证了查找的效率,杜绝了BST的局限性

这里先说明结论,红黑树就是2-3-4树,理解了2-3-4树就理解了红黑树的80%。这里仅介绍2-3-4树的各种操作的思想和方式,并不书写代码实现。

区别于二叉树2-3-4树允许存在3-节点和4-节点

首先2-节点,也就是正常二叉树中的节点,其包含一个节点和两个链接

图2-1 2-节点

3-节点拥有两个节点,和3条链接

图2-2 3-节点

4-节点拥有3个节点和4条链接

图2-3 4-节点

2-3-4树的插入操作

2-3-4树的插入永远都是将当前节点与待插入位置的父节点合并来完成。

例如待插入位置的父节点为一个2-节点,就需要把当前节点和这个2-节点进行合并成为一个3-节点

图2-4 插入到2节点

若插入位置父节点为一个3-节点就将其组合为一个4-节点

图2-5 插入到3节点

那么如果待插入节点的位置为一个4-节点,那么情况就有些复杂

我们需要先将这个4-节点裂变为3个2-节点

图2-6 插入到3节点1

接着再将插入节点和合适的子节点(A或C)进行结合

图2-7 插入到3节点2

但是请注意这里有一个问题我们将P的右子节点裂变后高度增加了1,整个树就不平衡了,此时我们需要把B节点和其父节点进行结合。这里结合又存在3种情况了那就是P节点的三种状态2-节点、3-节点或4-节点2-或3-节点直接结合即可,若为4-节点那么就还需要向上裂变,直到到达根节点,若到达根节点还需要裂变则不需要继续向上结合,此时2-3-4树的高度增加1,这也是2-3-4树唯一的增加高度的方式。

我们将上图的B和P进行合并,就完成了这一次的增加操作:

图2-8 插入到3节点3

2-3-4树的删除操作

删除也分为大概2种情况

这里的删除我们先只考虑删除叶子节点,原因会在后面与红黑树对应时解答

首先是删除一个3-或4-节点中的元素,这种情况直接删除即可,因为不影响树的高度。例如下图的删除A,当然删除B效果也一样

图2-9 删除3-或4-节点

删除一个2-节点是比较复杂的,我们通常选择的方案是将这个2-节点变为3-或4-节点然后再将其删除

第1种情况:当前节点的兄弟节点是一个3-或4-节点,那么可以和兄弟借节点将本节点变为3-或4-节点

图2-10 删除2-节点1

第2种情况:当前节点的兄弟节点也是一个2-节点,需要找父亲借,将父亲的一个节点和当前节点和兄弟节点组合在一起,成为一个4-节点,再删除A即可

图2-11 删除2-节点2

这里还有一个更加难理解的情况,就是父节点是不为根节点的2-节点怎么办,我们把父节点借过来,对于祖父节点来说父节点这一个分支高度减少了1,整棵2-3-4树就不平衡了。所以我们要对父节点进行调整。

图2-12 删除2-节点3

但是你可以发现这次的调整也是一次类似删除的过程,若祖父节点还是一个2-节点那么我们还序号将祖父作为删除节点向上调整直到根节点。到这里也就可以大体明白了,2-3-4树的删除2-节点是一个自下而上的过程。这里如果没理解删除的自下而上的流程没有关系,在后面的红黑树对应2-3-4树的章节中还会将这里的删除与红黑树对应,理解起来会容易些。

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

对应关系

我们将上面介绍的2-3-4树的3-节点和4-节点用黑节点+红节点表示,即可将2-3-4树转化为红黑树

图3-1 2-3-4树转化红黑树

下面来介绍一下红黑树种对于2-3-4树的对应关系

首先是2-3-4树的2-节点,在红黑树中直接表示为黑节点

3-节点有两种表达方式,若将红节点放在右边称之为右倾,而红节点放在左边称为左倾

图3-2 2-3-4树转化红黑树3-节点

4-节点的表示比较简单,为一个黑节点连接两个红节点

图3-3 2-3-4树转化红黑树4-节点

推导红黑树的性质

先说两个必须确定的性质:

1 每个节点不是红色就是黑色

2 叶子节点为黑色(这里的叶子节点指的是为树的最底端指向的null节点)

先来确认一个简单的性质,3 根节点为黑色。这一点也是必须的,在2-3-4中根节点只能为为2-或3-或4-节点。2-节点转化到红黑树中就是黑色的,3-节点转化为红黑树节点为一个黑节点连接一个红节点,我们一般将这个黑节点作为根节点,而4-节点转化为一个黑节点连接两个红节点,也是将这个黑节点设置为根节点。

4 红节点必有两个黑色的子节点,这里是将指向null也算作黑节点,这也是性质2所表达的含义。

首先2-3-4树是完美平衡的树,即同一高度的节点要么都没有子节点要么都有两个子节点。根据转换方案,红色节点都是用来代替2-3-4树中的3-或4-节点的那么它们也就满足要么都没有子节点要么就有两个子节点这一特点。

5 对于任意节点,其到叶子节点的距离包含相同数量的黑节点,这一点也不难理解,在2-3-4树中对于2-节点3-节点4-节点我们都用且最多一个黑节点表示,根据2-3-4树完美平衡的结论,我们忽略红节点后红黑树就是完美平衡的树。

2-3-4树对应红黑树插入

首先确认一点,红黑树中插入的节点都是红色的。这源于"2-3-4树的插入永远都是将当前节点与待插入位置的父节点合并来完成"这句话,和其他节点合并的操作在红黑树中其实就是黑节点连接红节点。

第一种情况,插入到一个2-节点

图3-4 2-3-4树对应红黑树插入到2-节点

第二种情况插入到3-节点,这里又分成两种情况也就是插入到3-节点中的黑节点下面和红节点下面

先看插入到黑节点下面,直接插入即可

图3-5 2-3-4树对应红黑树插入到3-节点1

插入到红节点下面就稍微复杂一点,插入完成后需要进行一次旋转进行调平,这里是左旋转,如果是两个子节点连接在左子节点那么就得进行右旋转

图3-6 2-3-4树对应红黑树插入到3-节点2

当然还有一种情况就是我们要向本就右倾的3-节点插入到右子节点的左子节点,需要先将其调整到右子上也就是调整为上图的形式

图3-6-2 2-3-4树对应红黑树插入到3-节点2

看完上面的旋转操作有的小伙伴可能有点懵,不要慌,我们先来补全旋转的知识。

首先记住结论,旋转不影响顺序,左旋会使右子树高度减少左子树高度增加,右旋相反。即旋转是保证了有序性的高度调整。

先来看左旋的操作,具体操作为

  1. 当前节点的右子节点的左子节点指向当前节点
  2. 当前节点的右子节点指向右子节点的左子节点

完整的旋转代码点击此处跳转到底部代码清单1-4查看

图3-7 左旋转

右旋转的操作和左旋转是相反的

  1. 当前节点的左子节点的右子节点指向当前节点
  2. 当前节点的左子节点指向当前节点的右子节点

完整的旋转代码点击此处跳转到底部代码清单1-4查看

图3-8 左旋转

接下来我们来看插入中最复杂的一个操作。第三种情况,向一个4-节点中插入节点

首先还是将BCD节点构成的4-节点进行裂变,其实就是BD变为黑色

接着将A插入到B的左边

将C与P进行结合,其实就是将C变为红色

图3-9 2-3-4树对应红黑树插入到4-节点1

此处其实有一个很严重的问题,也就是向上裂变的操作,如果P属于某个3-或4-节点,那么我们就需要重复之前的操作也就是判断P是2-节点还是3-节点亦或者4-节点2-节点最好就是不用操作,若为3-节点有可能不用操作也有可能需要进行旋转。若P为4-节点那么就需要继续向上裂变,直到根节点。如果裂变到根节点就不需要再将其与其父节点合并了,也就是不需要变为红色了,也正印证了根节点必须为黑色。

2-3-4树对应红黑树删除

我们先来分析为何在2-3-4树的删除情况时只讨论删除叶子节点。其实如果看过之前的BST删除章节这点不难理解。我们会将删除的情况分为三种。

  • 有两个子节点
  • 有一个子节点
  • 没有子节点

我们会将删除两个子节点的情况转化为后两种情况,具体方法请参考之前BST的删除章节。这一点对于红黑树当然也适用,对于后两种情况,若将它们处于的树转化为2-3-4树后它们的位置必为2-3-4树的叶子节点。故而我们只需要考虑删除2-3-4树的叶子节点的情况。但是这不代表这很容易。

首先来看第一种情况删除3-或4-节点中的节点。如果删除4-节点中的节点直接删除即可。例如下图不可能存在删除B的情况,因为会使用其后继节点代替它,所以只存在删除A或C,删除红节点无需考虑平衡问题

图3-10 2-3-4树对应红黑树删除到4-节点

然后来看删除3-节点的情况,其会分出两种情况。删除A或删除B,删除红节点A直接删除即可,删除B则需要对B进行右旋再删除B

图3-11 2-3-4树对应红黑树删除到3-节点

最难的就是删除2-节点,因为2-节点必为黑色。删除一个黑色节点红黑树会失衡。根据我们之前的2-3-4树的理论,我们需要先向兄弟节点借。

我们来先引入一个红黑树删除中的实际问题,就是如何得到兄弟节点。最简单的方案就是若当前节点是父节点的左子节点那么兄第节点就是父节点的右子节点,反之亦然。但是这个逻辑有个问题,这里的兄弟节点定义的是2-3-4树的兄弟节点,这就会出现一些问题

例如将下图左变2-3-4树转化为红黑树就会有形态一或形态二的等价关系

图3-12 2-3-4树对应红黑树删除到2-节点1

此时我们如果删除A节点,我们使用之前设想的逻辑,在形态二中可以找到真正的兄弟节点D。但是在形态一种找到的却是E,并非真正的兄弟节点。当然我们也可以再沿着左子树找到C,但是那样操作太复杂了。我们可以使用一次旋转将形态一变为形态二。

也就是说一旦在查找兄弟节点时,兄弟节点为红色,那么就代表这个兄弟节点并非要查找的兄弟节点。此时对父节点进行一次适当的旋转,例如上图的形态一对B进行一次左旋转。即可得到形态二。此时再查找A的兄弟节点就会找到正确的C节点

图3-13 2-3-4树对应红黑树删除到2-节点2

下面就来一一解决各种情况,这里先假设已经找到真正的兄弟节点,倘若兄弟节点有的借。

若兄弟节点是一个4-节点也就是有两个红子节点,那样最好。

但兄弟节点是一个3-节点,就要分情况处理了

  • 若删除节点是父节点的左子节点,我们一般期望3-兄弟节点是一个右倾状态

  • 若删除节点是父节点的右子节点,我们一般期望3-兄弟节点是一个左倾状态

这里考虑这两种情况的原因是考虑到旋转操作的特点。若删除节点是父节点的左子节点,我们一般是通过左旋转来降低兄弟节点高度,提升当前节点的高度,从而维持删除本节点后的平衡。

可以看到如果兄弟节点是左子节点为红,左旋转后出现了问题D节点的右子树为空了。一般来说我们会将C变为黑色且继续留在D的右子节点上,这一我们从右子树中拿走了一个黑节点又补上一个黑节点。所以我们在处理删除是父节点左子节点时最好保证其兄弟节点为右倾。当然右面的情况异曲同工,这里就不演示了。后面可以在代码中体现出来。

图3-14 2-3-4树对应红黑树删除到2-节点3

接下来就是遇到处理父节点为左子节点且兄弟节点为左倾时怎么办,其实只需要对兄弟节点进行一次右旋转即可将左倾变为右倾。当然相反的操作也一样。

最后就是兄弟节点没得借的情况,需要向父节点借,这里其实是最难理解的部分,但是其实逻辑又很简单。

这里再分为两种情况,父节点是2-节点或父节点是3-4-节点,这里可以通过判断父节点的颜色来判断。若这里是3-节点之前的找真正的兄弟节点操作会强行将父节点变为红色,若为4-节点本就是红色。只有2-节点会是黑色。

若父亲是红色的,其实也就是父亲有的借,我们只需要将兄弟节点变为红色,父节点变为黑色即可,因为A是要删除的所以并不需要操作

图3-15 2-3-4树对应红黑树删除到2-节点4

问题在于若父亲是黑色的节点,也就是没得借咯。试想一下我们将兄弟变为红色,而本节点又要删除,这不就是父亲代表的这棵树高度-1。若父亲是红色的变为黑色那么高度就回来了,若父本来就是黑色的就没招了,需要利用父亲向上继续执行调整操作。去借叔叔的或者祖父的节点,直到借到根节点停止。

上述对于关系基本是将红黑树的各种情况说清楚了,但是对于自己实现一颗红黑树还是有些距离的,接下来的两节,会带大家分析TreeMap的源码红黑树的插入和删除实现,当然具体的代码做的步骤可能和上述讲述的流程不同(代码主要考虑了各种情况的概括,也就是一段代码做了上述情况的很多需求)。

当然实现红黑树的代码方案有很多种,比如算法第四版的插入算法仅写了11行就实现了,而TreeMap用了100多行,实现方案有很多。我们只需要去学习大佬们实现方案中的思想和技巧即可。

TreeMap红黑树插入实现分析

准备工作

首先我们来修改一下BST的节点,将其变为红黑树的节点。其实仅仅多了一个颜色属性。

完整的红黑树结构代码点击此处跳转到底部代码清单1-5查看

1
private boolean color;

且在外部的红黑树类中定义红色和黑色静态变量用于判断

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

BST插入为红黑树插入做了什么

我们可以将红黑树的插入分为两个步骤,插入和插入后的自平衡。

这里的插入其实就是之前BST中的插入流程。具体过程可以查看第一节的BST插入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public void put(K key, V val) {
    var t = this.root;
    if (t == null) {
        root = new Entry<>(key, val, null);
        return;
    }
    // 寻找插入位置
    int cmp;
    Entry<K, V> parent;
    if (key == null) throw new NullPointerException();
    // 沿着根节点寻找插入位置
    do {
        parent = t;
        cmp = key.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else {
            t.val = val;
            return;
        }
    } while (t != null);
    // 插入节点
    var e = new Entry<>(key, val, parent);
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    // 调整
    fixAfterPut(e);
}

和BST不同的是红黑树需要在插入后进行调平操作。我们将这个操作抽取为一个方法,下面我们一步步分析所有情况,写出这个方法。

完整的红黑树插入代码点击此处跳转到底部代码清单1-6查看

情况1:父节点为黑色

这种情况不需要任何操作,对应2-3-4树中的2-节点和正确倾斜的3-节点,如下图插入A节点

图4-1 红黑父节点为黑色

情况2:父节点为红色

这说明遇到了3-节点错误的倾斜或者4-节点的情况。注意下面的叔叔节点指的就是红黑树中的叔叔节点,并非2-3-4树的叔叔节点

我们可以使用一个判断来决定接下来的代码块。

1
if (x != null && x.parent.color == RED) {} // x代表当前插入的节点

这里先将父节点为祖父的左子或右子的情况分开,因为这两种情况的进行的操作是反过来的,所以分开操作

直接使用判断分开父亲为祖父的左子和右子的情况

1
2
3
4
5
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // x的父亲是否是其祖父的左子节点
    // 左子的情况2-1
} else { // 其他情况,也就是其祖父的右子节点
    // 右子的情况2-2
}

情况2-1:父节点为祖父的左子节点

由于接下来的情况都需要叔叔节点的判定,这里先求出叔叔节点

1
var n = rightOf(parentOf(parentOf(x))); // 注意此处的前提是父亲节点为祖父节点的左子节点,故而叔叔节点为祖父的右子节点

情况2-1-1:叔叔节点为红色

此种情况,说明我们遇到了4-节点,此时需要将4-裂变为3个2-节点。其操作很简单,也就是将父亲和叔叔节点变为黑色。将祖父节点设为红色

最后将祖父当作插入节点,进行调整即可。这也是向上调整的

图4-2 红黑父节点为红色且叔叔节点为红色

1
2
3
4
5
6
7
8
9
if (colorOf(n) == RED) { // 叔叔节点为红色
    setColor(parentOf(x), BLACK); // 父亲为黑色
    setColor(n, BLACK); // 叔叔为黑色
    setColor(parentOf(parentOf(x)), RED); // 祖父设为红色
    // 此处需要将祖父节点当作插入节点继续调平
    x = parentOf(parentOf(x));  // 此处较难理解可以看下方的解释
} else {
    // 叔叔节点不为红的情况
}

x = parentOf(parentOf(x))这一句代码当然没办法做到继续将祖父当作插入节点,因为我们的整个调平是以一个判断开始的。我们需要使用一个循环开始,才可以做到持续对x进行判断。

将我们上述判断的开始条件作为循环的条件

1
2
3
while (x != null && x.parent.color == RED) {
    // 具体情况
}

但是光有上面两种条件是不行的,因为我们的向上裂变操作,到达根节点时就会停止,不会对根节点再进行调整。

所以这里我们加上不为根节点的判断,但是注意我们上面的代码会将祖父设为红色后再对其调整,也就是即使遇到根不进行调整,它也是红色的。我们需要在循环结束后将根节点颜色设为黑色。

1
2
while (x != null && x != root && x.parent.color == RED) {}
root.color = BLACK;

情况2-1-2:当前节点是父节点的右子节点

图4-3 叔叔节点不存在且父节点为祖父的左子节点

对父节点进行左旋,即可将本情况变为情况2-2-3

1
2
3
4
5
6
7
8
9
if (colorOf(n) == RED) {
    ...
} else {
    // 叔叔节点为空
    if (x == rightOf(parentOf(x))) { // 当前节点是父节点的右子节点也就是2-1-2情况
         x = parentOf(x); // x设置为父节点
         leftRotate(x); // 对x进行右旋,恰好x变为插入节点,可以看图中操作A节点
    }
}

情况2-1-3:当前节点是父节点的左子节点

到达此情况后我们看到的是连续的两个左子节点连接的红节点,此时只需要进行,旋转和变色,将两个红节点分配到祖父节点的左子和右子。

将祖父节点变为红色,父节点变为黑色。对祖父节点进行右旋即可,具体可以看上图。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (colorOf(n) == RED) {
    ...
} else {
    ...
    // 此时已经为2-1-3情况
    // 父亲总是要变为黑色的,祖父也是要变为红色
    setColor(parentOf(x), BLACK);
    setColor(parentOf(parentOf(x)), RED);
    // 祖父右旋
    rightRotate(parentOf(parentOf(x)));
}

情况2-2:父节点为祖父的右子节点

父节点为祖父的右子节点的情况。基本原理和左子的情况相同,区别就在于操作都是反过来的。

情况2-2-1:叔叔节点为红色

父节点和叔叔点设为黑色,祖父节点设为红色,将当前节点设为祖父节点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 叔叔节点
var n = leftOf(parentOf(parentOf(x)));
if (colorOf(n) == RED) {
    setColor(parentOf(x), BLACK); // 父亲为黑色
    setColor(n, BLACK); // 叔叔为       黑色
    setColor(parentOf(parentOf(x)), RED); // 祖父设为红色
    x = parentOf(parentOf(x));
} else {
    ...
}

情况2-2-2:当前节点为父节点的左子节点

图4-4 叔叔节点不存在且父节点为祖父的右子节点

需要将此情况转化为情况2-2-2,直接对父节点进行右旋。

1
2
3
4
5
6
7
8
if (colorOf(n) == RED) {
    ...
} else {
    if (x == leftOf(parentOf(x))) { // 当前节点是父节点的左子节点
        x = parentOf(x); // x设置为其父节点
        rightRotate(x); // 对x进行右旋
    }
}

情况2-2-3:当前节点为父节点的右子节点

将祖父节点设为红色,父节点设为黑色,对祖父进行左旋

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (colorOf(n) == RED) {
    ...
} else {
    ...
    // 父亲总是要变为黑色的,祖父也是要变为红色
    setColor(parentOf(x), BLACK);
    setColor(parentOf(parentOf(x)), RED);
    // 祖父左旋
    leftRotate(parentOf(parentOf(x)));
}

TreeMap红黑树删除实现分析

如果你看懂了上述的红黑树的插入实现,那么恭喜你红黑树的删除会更难。

完整的红黑树删除代码点击此处跳转到底部代码清单1-7查看

删除的步骤分析—何时需要调平

和插入一样,我们也将删除分为删除节点和删除后的自平衡。先来分析一下何时进行自平衡操作,和通过谁进行自平衡。

我们先将BST的删除代码拿过来。我们的删除情况名义上有三种,单实际中会转化为两种,删除只有一个子节点的节点和删除叶子。具体解析可以查看BST删除章节。

对于有一个子节点的节点删除,我们可以利用其替换节点也就是子节点进行调平。也就是下述代码第一句注释下的两个语句fixAfterRemove方法为对某个节点进行调平,之后我们会分析并实现这个方法。

而对于没有子节点的节点删除,是没有替换节点的,我们需要直接将其删除。那么我们的调平就没有目标。所以需要先对待删除节点进行调平,之后再将其删除。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void deleteNode(Entry<K, V> node) {
    if (node.left != null && node.right != null) {
        var successor = successor(node);
        node.key = successor.key;
        node.val = successor.val;
        node = successor;
    }
    var replacement = node.left != null ? node.left : node.right;
    if (replacement != null) {
        replacement.parent = node.parent;
        if (node.parent == null)
            root = replacement;
        else if (node == node.parent.left)
            node.parent.left = replacement;
        else node.parent.right = replacement;

        node.left = node.right = node.parent = null;
        // 若删除节点是黑色就对它的替换节点进行调整
        if (node.color = BLACK)
            fixAfterRemove(replacement);
    } else if(node.parent == null) {
        root = null;
    } else {
        // 若待删除节点为黑色对待删除节点先进行调整
        if (node.color == BLACK)
            fixAfterRemove(node);
        
        if (node.parent != null) {
            if (node == node.parent.left)
                node.parent.left = null;
            else if (node == node.parent.right)
                node.parent.right = null;
            node.parent = null;
        }
    }
}

下面我们就通过对各种情况的分析来一步步实现调平方法fixAfterRemove。

明确向上调平的实现方案

根据之前2-3-4删除对应红黑树删除的分析中,我们在删除一个2-节点时若其兄弟为2-且父亲也为2-节点时,需要将其合并为一个4-节点再删除,我们是需要利用父节点进行向上的递归调整的。作为递归调整的实现,和插入一样这里也是使用循环实现。

我们上面的删除方法deleteNode,要求我们的调平方法能够应对两种情况,也就是先删除后用子节点代替后的调平子节点和没有子节点先对其本身调平后删除其本身。对于前者,其实不难象到替换的子节点必为红色,故而不能进入循环体,但是我们需要将其变为黑色来填补删除的黑节点。所以在底部加入一句将x设为黑色。

这里的将x设为黑色,并非只有上述的这一点作用。后面会介绍他还起到什么作用。

1
2
while (x != root && colorOf(x) == BLACK) {} // x代表待调整节点
setColor(x, BLACK);

情况1:当前节点是父节点的左子节点

找到真正的兄弟节点

注意这里所定义的兄弟节点为2-3-4树中的兄弟节点,和插入中的定义有歧义请注意区分。

1
var sib = rightOf(parentOf(x));

rightOf(parentOf(x))这句代码找到的不一定是兄弟节点,例如下图的前面这颗红黑树删除A节点,其兄弟节点按照上述求法为E,其实对于2-3-4树真正的兄弟节点是C和D这个节点。这里做的操作是将父节点B进行一次左旋,此时的上述表达式求得得答案就为真正得兄弟节点。也就是下图得右面这棵树。

1
2
3
4
5
6
if (colorOf(sib) == RED) { // 兄弟节点为红色,就代表这个sib并非真正的兄弟节点
    setColor(sib, BLACK); // 兄弟节点设为黑色
    setColor(parentOf(x), RED); // 父节点设为红色
    leftRotate(parentOf(x)); // 对父节点进行左旋
    sib = rightOf(parentOf(x)); // 重新指定兄弟节点
}

图3-13 2-3-4树对应红黑树删除到2-节点2

情况1-1:兄弟节点没有子节点

此情况也就是需要向父亲借的过程。

colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK这里的判断是否为黑色其实是判断是否为空,具体原因可以查看下方的colorOf方法实现。

1
2
3
4
5
6
7
8
if (colorOf(leftOf(sib)) == BLACK
    && colorOf(rightOf(sib)) == BLACK
   ) {
    setColor(sib, RED);
    x = parentOf(x);
} else {
    ...
}

判断体中两句代码看似轻易,其实理解起来十分困难。

首先将兄弟节点设为红色,根据将4-节点裂变为3个2-节点的方案,这里也需要将待删除节点设为红色,这里并没有这样做,原因是待删除节点过会需要被删除,故而颜色修不修改的不影响。

接下来需要做的工作就是将父节点当作调整节点进行调整,也就是对应代码x = parentOf(x)。当然这里还忽略了父节点有节点给我们借。如果父节点为红色就代表有的借,若为黑色就代表父节点必为一个2-节点,因为我们也在上面的情况中屏蔽了兄弟节点为红色的情况。

父节点为红色进行新一轮的循环,循环体会结束(因为有当前节点为黑色的条件)。到达结尾的将x设为黑色,此时的将x设为黑色刚好代表从父亲的红色节点借到一个黑色节点,正好将树平衡。

而父节点为黑色,就会进入循环体,继续假设将当前节点删除后的调平,(这也是之前为什么不把待删除节点变红的另一重原因),此处就是向上调平的过程。

情况1-2:兄弟右子节点是黑色

对于删除节点是左子的情况,我们需要兄弟节点有一个右子节点。若右子节点不为红色,那么必有红色的左子节点,我们需要对兄弟节点进行右旋来获得一个红色的右子节点

1
2
3
4
5
6
7
if (colorOf(rightOf(sib)) == BLACK) {
    // rightRotate
    setColor(leftOf(sib), BLACK);
    setColor(sib, RED);
    rightRotate(sib);
    sib = rightOf(parentOf(x));
}

情况1-3:兄弟有红色的右子节点

到达本情况时,兄弟必有一个红色的右子节点,我们无法确定父节点的颜色所以将兄弟节点和父节点交换颜色。将兄弟节点的右子红节点设为黑色。最后对父节点进行左旋。

1
2
3
4
5
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
leftRotate(parentOf(x));
x = root;

情况2:当前节点是父节点的右子节点

此情况和情况1的思想基本相同,区别仅为操作相反,故而这里仅贴上代码

找到真正的兄弟节点

1
2
3
4
5
6
7
8
var sib = leftOf(parentOf(x));
// sib not is ture brother node
if (colorOf(sib) == RED) {
    setColor(sib, BLACK);
    setColor(parentOf(x), RED);
    rightRotate(parentOf(x));
    sib = leftOf(parentOf(x));
}

情况2-1:兄弟节点没有子节点

1
2
3
4
5
6
if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {
    setColor(sib, RED);
    x = parentOf(x);
} else {
    ...
}

情况2-2:兄弟左子节点是黑色

1
2
3
4
5
6
if (colorOf(leftOf(sib)) == BLACK) {
    setColor(rightOf(sib), BLACK);
    setColor(sib, RED);
    leftRotate(sib);
    sib = leftOf(parentOf(x));
}

情况2-3:兄弟有红色的左子节点

1
2
3
4
5
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rightRotate(parentOf(x));
x = root;

代码清单

1-1BST的Node节点设计

  • 注,未设置Get/Set方法的原因是一般情况我们会将节点类设置为静态内部类,故而不需要Get/Set方法。而本例下的代码也都是将Entry类作为实现BST类的静态内部类处理的。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Entry<K extends Comparable<K>, V> {
    private K key;
    private V val;
    private Entry<K, V> left;
    private Entry<K, V> right;
    private Entry<K, V> parent;
    public Entry(K key, V val, Entry<K, V> left, Entry<K, V> right, Entry<K, V> parent) {
        this.key = key;
        this.val = val;
        this.left = left;
        this.right = right;
        this.parent = parent;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Entry<?, ?> entry = (Entry<?, ?>) o;
        return key.equals(entry.key);
    }
    @Override
    public int hashCode() {
        return Objects.hash(key);
    }
    @Override
    public String toString() {
        return "Entry{" +
                "key=" + key +
                ", val=" + val +
                '}';
    }
}

1-2BST插入代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void put(K key, V val) {
    if (key == null) throw new NullPointerException();
    
    var t = this.root;
    if (t == null) {
        root = new Entry<>(key, val, null);
        return;
    }
    // 寻找插入位置
    int cmp;
    Entry<K, V> parent;
   
    // 沿着根节点寻找插入位置
    do {
        parent = t;
        cmp = key.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else {
            t.val = val;
            return;
        }
    } while (t != null);

    // 插入节点
    var e = new Entry<>(key, val, null, null, parent);
    if (cmp < 0) parent.left = e;
    else parent.right = e;
}

1-3BST删除代码

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// 删除
public V remove(K key) {
    var node = getNode(key);
    if (node == null) return null;
    V oldVal = node.val;
    deleteNode(node);
    return oldVal;
}
// 删除节点
private void deleteNode(Entry<K, V> node) {
    // node 节点有两个孩子
    if (node.left != null && node.right != null) {
        var successor = successor(node);
        node.key = successor.key;
        node.val = successor.val;
        node = successor;
    }
    var replacement = node.left != null ? node.left : node.right;
    if (replacement != null) {
        replacement.parent = node.parent;
        if (node.parent == null)
            root = replacement;
        else if (node == node.parent.left)
            node.parent.left = replacement;
        else node.parent.right = replacement;

        node.left = node.right = node.parent = null;
        if (node.color = BLACK)
            fixAfterRemove(replacement);
    } else if(node.parent == null) {
        root = null;
    } else {
        // 叶子节点直接删除
        if (node.color == BLACK)
            fixAfterRemove(node);

        if (node.parent != null) {
            if (node == node.parent.left)
                node.parent.left = null;
            else if (node == node.parent.right)
                node.parent.right = null;
            node.parent = null;
        }
    }
}
// 获取节点
private Entry<K, V> getNode(K key) {
    if (key == null)
        throw new NullPointerException();
    var node = root;
    while (node != null) {
        int cmp = key.compareTo(node.key);
        if (cmp < 0)
            node = node.left;
        else if (cmp > 0)
            node = node.right;
        else return node;
    }
    return null;
}
// 查找后继节点
private Entry<K, V> successor(Entry<K, V> h) {
    if (h == null) return null;
    else if (h.right != null) {
        var p = h.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        // 删除情况下不会到这里
        var p = h.parent;
        var ch = h;
        while (p != null && ch != rightOf(p)) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}
// 查找前驱节点
private Entry<K, V> predecessor(Entry<K, V> h) {
    if (h == null) return null;
    else if (h.left != null) {
        var p = h.left;
        while (p.right != null)
            p = p.right;
        return p;
    } else {
        // 删除情况下不会到这里
        var p = h.parent;
        var ch = h;
        while (p != null && ch != leftOf(p)) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

// 辅助方法
// 获取父节点
private Entry<K, V> parentOf(Entry<K, V> h) {
    return h != null ? h.parent : null;
}
// 获取左子节点
private Entry<K, V> leftOf(Entry<K, V> h) {
    return h != null ? h.left : null;
}
// 获取右子节点
private Entry<K, V> rightOf(Entry<K, V> h) {
    return h != null ? h.right : null;
}

// BST不会用到,会在实现红黑树时使用
// 获取节点颜色
private boolean colorOf(Entry<K, V> h) {
    return h == null ? BLACK : h.color;
}
// 设置节点颜色
private void setColor(Entry<K, V> h, boolean color) {
    if (h != null) h.color = color;
}

1-4旋转操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 左旋操作
private void leftRotate(Entry<K, V> p) {
    if (p != null) {
        var r = p.right; // 存储当前节点的右子节点
        p.right = r.left; // 当前节点的右子节点设为右子节点的左子节点
        if (r.left != null)
            r.left.parent = p; // 设置右子节点的左子节点的父节点为当前节点
        r.parent = p.parent; // 设置右子节点的父节点为当前父节点
        if (p.parent == null)
            root = r; // 若当前节点父节点为空要将root替换为右节点
        else if (p.parent.left == p)
            p.parent.left = r; // 当前节点是父节点的左子节点,将父节点的左子设为当前节点的右子节点
        else
            p.parent.right = r; // 当前节点是父节点的右子节点,将父节点的右子设为当前节点的右子节点

        r.left = p; // 当前节点的右子节点的左子节点设为当前节点
        p.parent = r; // 当前节点的父节点设置为当前节点右子节点
    }
}
// 右旋,和左旋是反过来的
private void rightRotate(Entry<K, V> p) {
    if (p != null) {
        var l = p.left;
        p.left = l.right;
        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;
    }
}

1-5红黑树结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class RBTree<K extends Comparable<K>, V> {

    private Entry<K, V> root;

    private static final boolean RED = false;
    private static final boolean BLACK = true;
    
    static class Entry<K extends Comparable<K>, V> {
        private K key;
        private V val;
        private Entry<K, V> left;
        private Entry<K, V> right;
        private Entry<K, V> parent;
        private boolean color;
        public Entry(K key, V val, Entry<K, V> left, Entry<K, V> right, Entry<K, V> parent) {
            this.key = key;
            this.val = val;
            this.left = left;
            this.right = right;
            this.parent = parent;
            this.color = RED;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Entry<?, ?> entry = (Entry<?, ?>) o;
            return key.equals(entry.key);
        }
        @Override
        public int hashCode() {
            return Objects.hash(key);
        }
        @Override
        public String toString() {
            return "Entry{" +
                "key=" + key +
                ", val=" + val +
                '}';
        }
    }
}

1-6红黑树插入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public void put(K key, V val) {
    if (key == null) throw new NullPointerException();
    var t = this.root;
    if (t == null) {
        root = new Entry<>(key, val, null, null, null);
        return;
    }
    // 寻找插入位置
    int cmp;
    Entry<K, V> parent;
    // 沿着根节点寻找插入位置
    do {
        parent = t;
        cmp = key.compareTo(t.key);
        if (cmp < 0)
            t = t.left;
        else if (cmp > 0)
            t = t.right;
        else {
            t.val = val;
            return;
        }
    } while (t != null);
    // 插入节点
    var e = new Entry<>(key, val, null, null, parent);
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    // 调整
    fixAfterPut(e);
}

private void fixAfterPut(Entry<K, V> x) {
    x.color = RED;
    while (x != null && x != root && x.parent.color == RED) {
        // 1. x父节点是祖父的左节点
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // 叔叔节点
            var n = rightOf(parentOf(parentOf(x)));
            /*
             * 叔叔节点为红色,就代表现有节点要插入一个4-节点,源4-节点需要进行裂变
             * 在红黑树中使用变色完成裂变操作
             * */
            if (colorOf(n) == RED) {
                setColor(parentOf(x), BLACK); // 父亲为黑色
                setColor(n, BLACK); // 叔叔为黑色
                setColor(parentOf(parentOf(x)), RED); // 祖父设为红色
                x = parentOf(parentOf(x));
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    leftRotate(x);
                }
                // 父亲总是要变为黑色的,祖父也是要变为红色
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                // 祖父右旋
                rightRotate(parentOf(parentOf(x)));
            }
        } else {
            // 叔叔节点
            var n = leftOf(parentOf(parentOf(x)));
            if (colorOf(n) == RED) {
                setColor(parentOf(x), BLACK); // 父亲为黑色
                setColor(n, BLACK); // 叔叔为黑色
                setColor(parentOf(parentOf(x)), RED); // 祖父设为红色

                x = parentOf(parentOf(x));
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rightRotate(x);
                }
                // 父亲总是要变为黑色的,祖父也是要变为红色
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                // 祖父左旋
                leftRotate(parentOf(parentOf(x)));
            }
        }
    }

    root.color = BLACK;
}

1-7红黑树删除

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public V remove(K key) {
    var node = getNode(key);
    if (node == null) return null;

    V oldVal = node.val;

    deleteNode(node);

    return oldVal;
}

private void deleteNode(Entry<K, V> node) {
    // node 节点有两个孩子
    if (node.left != null && node.right != null) {
        var successor = successor(node);
        node.key = successor.key;
        node.val = successor.val;
        node = successor;
    }

    var replacement = node.left != null ? node.left : node.right;

    if (replacement != null) {
        replacement.parent = node.parent;
        if (node.parent == null)
            root = replacement;
        else if (node == node.parent.left)
            node.parent.left = replacement;
        else node.parent.right = replacement;

        node.left = node.right = node.parent = null;
        if (node.color = BLACK)
            fixAfterRemove(replacement);
    } else if(node.parent == null) {
        root = null;
    } else {
        // 叶子节点直接删除
        if (node.color == BLACK)
            fixAfterRemove(node);

        if (node.parent != null) {
            if (node == node.parent.left)
                node.parent.left = null;
            else if (node == node.parent.right)
                node.parent.right = null;
            node.parent = null;
        }
    }
}

private void fixAfterRemove(Entry<K, V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) { // left children
            // brother node
            var sib = rightOf(parentOf(x));
            // sib is red / in 2-3-4 find true sib node
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                leftRotate(parentOf(x));
                sib = rightOf(parentOf(x));
            }
            // brother not has
            if (colorOf(leftOf(sib)) == BLACK
                && colorOf(rightOf(sib)) == BLACK
               ) {
                setColor(sib, RED);
                x = parentOf(x);
            } else { // brother has node

                // 兄弟节点右子节点为黑或空
                if (colorOf(rightOf(sib)) == BLACK) {
                    // rightRotate
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rightRotate(sib);
                    sib = rightOf(parentOf(x));
                }

                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                leftRotate(parentOf(x));
                x = root;
            }

        } else { // right children
            // brother node
            var sib = leftOf(parentOf(x));

            // sib not is ture brother node
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rightRotate(parentOf(x));
                sib = leftOf(parentOf(x));
            }

            if (colorOf(leftOf(sib)) == BLACK && colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    leftRotate(sib);
                    sib = leftOf(parentOf(x));
                }
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rightRotate(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}
Powered by Hugo & Stack