红黑树

二叉树
二叉搜索树
AVL树
红黑树
字典树
哈夫曼树
线段树
笛卡尔树

示例练习>>>

AVL树 是高度平衡的二叉搜索树,使用它主要是为了方便查找,以及减少查询次数,但如果我们添加和删除的次数比较多的时候,使用他显然就不合适了,因为他频繁的旋转增加了算法的时间复杂度,这个时候我们就可以使用另外一种树就是红黑树,红黑树也会旋转但不会像 AVL树 那么频繁。红黑树有 5 个重要的性质。

  1. 节点是红色或黑色。
  2. 根节点是黑色。
  3. 所有叶子都是黑色。(叶子是null节点)
  4. 从根节点到所有的叶子节点的路径上不能有两个连续的红色节点。
  5. 从任一节点到其每个叶子的路径都包含相同数目的黑色节点。

其中第 3 条如果不理解,基本上是搞不懂红黑树的,他说的叶子不是二叉树中定义的叶子节点,是一种空的节点,他意思告诉我们任一节点如果只有一个子节点,那么该节点也是有两条路径的,这样就限制了另一条路径的长度。其实也在变相的告诉我们红黑树中如果一个节点只有一个子节点,那么这个子节点必定是红色的,结合第 4 和第 5 条就明白了。

红黑树中根到叶子的所有路径中,最长路径不会超过最短路径的两倍。我们可以反证一下,假设超过两倍会怎么样?比如最短路径长度是 a,最长路径长度是 2a+1 。根据性质 4 ,路径上不能有两个连续的红色节点,即便最短路径上全部都是黑色节点,那么在最长路径上也会有 a+1 个红色节点,因为根节点是黑色的,也就是说在剩下的 a-1 个黑色节点之间插入 a+1 个红色的节点,并且红色节点不能有连续的,无论如何也是做不到的,所以最长路径不可能超过最短路径的两倍。

红黑树的插入:

红黑树也是一棵二叉搜索树,他的查找,插入和删除我们可以参照前面讲的二叉搜索树和 AVL树 ,不过在插入和删除之后有可能打破树的平衡,所以在插入和删除之后还需要进行调整,我们先来看下红黑树插入的调整。

如果插入的是黑色节点,根据红黑树的性质 5 ,肯定会破坏这一规则,所以这里插入的节点都是红色的。但插入红色节点有可能会打破坏性质 4 ,也有可能不会。如果没有破坏就不需要调整,如果破坏我们在调整。下面来分别讨论插入遇到的几种情况,为了方便描述,我们把父节点的父节点称为爷爷节点,和父节点有共同父节点的称为叔叔节点。插入之后主要涉及涂色和旋转,这里只讨论插入节点的父节点是他爷爷的左子节点,如果是右子节点就不在介绍,因为原理都一样,就是左旋和右旋的区别。

1,插入的是根节点。

如果插入的是根节点,直接把它涂黑然后返回即可。

2,如果插入节点的父节点是黑色。

因为插入的节点是红色,如果插入节点的父节点是黑色,没有破坏任何一条性质,不需要做任何调整。

3.1,如果插入节点的父节点是红色,叔叔节点也是红色。

因为插入的节点是红色,父节点也是红色,所以破坏了性质 4 ,需要把父节点涂成黑色。因为父节点之前是红色的,所以他肯定不是根节点,插入的节点肯定有爷爷节点,并且爷爷节点一定是黑色的。把父节点涂成黑色之后,经过父节点的这条路径就多了一个黑色节点,根据性质 5 ,需要把爷爷节点涂成红色,但这样叔叔节点这条路径上少了一个黑色节点,所以还需要把叔叔节点涂成黑色。如果爷爷节点是根节点,还需要把它涂成黑色(根节点是黑色的),否则爷爷的父节点如果也是红色,就破坏了性质 4 ,需要继续往上判断,如下图所示。

总结:把父节点和叔叔节点涂成黑色,爷爷节点涂成红色,继续往上判断爷爷节点。

3.2,如果插入节点的父节点是红色,叔叔节点是黑色,当前节点是父节点的左子节点。

这种情况下也是先把父节点涂成黑色,因为父节点变成黑色之后,经过父节点的这条路径上就多了一个黑色节点,所以还需要把爷爷节点涂成红色。那么这里有个疑问,叔叔节点怎么处理?有的同学可能会说像 3.1 那样把叔叔节点也涂成黑色不就行了。其实这样是不行的,主要有两点,第一如果叔叔节点是空的怎么办,因为根据性质 3 ,空节点也是黑色的,所以这样就没法涂了。第二如果叔叔节点不是空,那么他就是从 3.1 转化过来的( 3.1 涂完之后会继续往上判断),这种情况下他本来就是黑色的,你在把它涂成黑色相当于没涂。既然不能改变叔叔节点的颜色,那么叔叔节点这条路径就比父节点这条路径少了一个黑色节点(因为爷爷节点变红了,叔叔节点这条路径就少了一个黑色的),唯一的解决方式就是对爷爷节点进行右旋,让父节点成为这棵子树的根节点,那么这个父节点黑色的个数就可以被他的两棵子树共享了,如下图所示。

总结: 父节点涂黑,爷爷节点涂红,对爷爷节点右旋,因为父节点已经变黑了就不在继续判断。

3.3,如果插入节点的父节点是红色,叔叔节点是黑色,当前节点是父节点的右子节点。

这种情况下和 3.2 中讲的类似,如果我们对爷爷节点进行右旋的话,会导致一条路径缺少了一个黑色节点,如下图所示。注意这里大家可能会有疑惑,插入的 X 节点怎么会有子节点?实际上这里的 X 节点有可能是插入的也有可能是 3.1 往上调整得到的,我们要放在一起看。

很明显直接对爷爷节点旋转是不行的,那该怎么办呢?其实我们可以对 X 的父节点进行左旋,就变成 3.2 中介绍的那样了,后面的就按照 3.2 中的那样处理就行了,如下图所示。

总结:X 节点指向父节点,对 X 节点左旋,后面参照 3.2

4.1,如果插入节点的父节点是红色,父节点是爷爷节点的右子节点。

这里也分几种情况,和上面类似,只不过是左旋和右旋的区别,这里就不在介绍。最后来看下代码。

// 如果是空节点就返回黑色。
private static <K, V> boolean colorOf(TreeMap.Entry<K, V> p) {
    return (p == null ? BLACK : p.color);
}

// 对插入元素的调整。
private void fixAfterInsertion(TreeMap.Entry<K, V> x) {
    x.color = RED;// 插入节点是红色的。

    // 只有父节点不为空且是红色才会操作。
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // 父节点是爷爷的左子节点。
            // y 是叔叔节点。
            TreeMap.Entry<K, V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {// 叔叔节点是红色,参考3.1。
                setColor(parentOf(x), BLACK);// 父节点涂黑。
                setColor(y, BLACK);// 叔叔节点也涂黑。
                setColor(parentOf(parentOf(x)), RED);// 爷爷节点涂红。
                x = parentOf(parentOf(x));// 继续往上判断爷爷节点。
            } else {
                // 当前节点是父节点的右子节点,参考3.3。
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);// X节点指向父节点。
                    rotateLeft(x);// 对父节点左旋,变成 3.2。
                }
                // 参考3.2。
                setColor(parentOf(x), BLACK);// 父节点涂黑。
                setColor(parentOf(parentOf(x)), RED);// 爷爷节点涂红。
                rotateRight(parentOf(parentOf(x)));// 对爷爷节点右旋。
            }
        } else {
            // 父节点是爷爷的右子节点,和上面类似。
            TreeMap.Entry<K, V> 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 {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    // 最后把根节点变成黑色。
    root.color = BLACK;
}

红黑树的删除:

删除操作可以参考 AVL树 的删除,我们也可以看下,这是红黑树 TreeMap 中的源码。

private void deleteEntry(TreeMap.Entry<K, V> p) {
    modCount++;
    size--;
    // 如果 p 有两个子节点,找到他的后继节点,移形换位。
    if (p.left != null && p.right != null) {
        // 查找 p 的后继节点 s。
        TreeMap.Entry<K, V> s = successor(p);
        // 把 s 的值赋值给 p ,相当于删除 s。
        p.key = s.key;
        p.value = s.value;
        p = s;// p 用 s 替换。
    } // p has 2 children

    // replacement 是 p 的子节点,p 最多只有一个子节点。
    TreeMap.Entry<K, V> replacement = (p.left != null ? p.left : p.right);
    // 判断 p 是不是叶子节点。
    if (replacement != null) {
        // p 不是叶子节点,先删除 p 节点。
        replacement.parent = p.parent;
        if (p.parent == null)
            root = replacement;
        else if (p == p.parent.left)
            p.parent.left = replacement;
        else
            p.parent.right = replacement;
        p.left = p.right = p.parent = null;

        // 如果删除的是黑色节点才调整,replacement节点必定是红色的。
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // p 是根节点。
        root = null;
    } else { //  p 是叶子节点。
        if (p.color == BLACK)// p 是黑色才会调整。
            fixAfterDeletion(p);
        // 删除 p 节点。
        if (p.parent != null) {
            if (p == p.parent.left)
                p.parent.left = null;
            else if (p == p.parent.right)
                p.parent.right = null;
            p.parent = null;
        }
    }
}

如果 p 有两个子节点,删除的时候不是直接删除 p 节点,而是用 p 的后继节点来替换 p 节点,然后删除 p 的后继节点。所以删除的节点要么是叶子节点要么只有一个子节点,不可能有两个子节点。我们看到上面代码中如果 p 的后继节点是叶子节点,是先调整 p 节点然后在删除,如果不是叶子节点,是先删除 p 节点然后在调整 replacement ,这里为什么不都是调整完之后在删除呢?这是因为删除调整的节点必须是黑色的,如果 p 不是叶子节点,他就只能有一个子节点 replacement,根据红黑树性质这个子节点必定是红色的,所以如果先把 p 节点删除,调整 replacement 节点的时候直接把它变成黑色,不需要做任何其他变色和旋转操作,简单省事。

private void fixAfterDeletion(TreeMap.Entry<K, V> x) {
    while (x != root && colorOf(x) == BLACK) {// 是黑色才会调整。
        if (x == leftOf(parentOf(x))) {
            // ……
        } else { // symmetric
            // ……
        }
    }
    setColor(x, BLACK);
}

fixAfterDeletion 函数到底是调整 p 节点还是调整 p 的子节点,我们不需要管,只需要记住在调整的这个节点的路径上少了一个黑色节点即可,有兴趣的大家可以自己看,这里就不带大家逐步分析了。

相关链接

数组
数组滚动数组差分数组树状数组
链表
链单向表双向链表循环链表跳表异或链表
队列
队列循环队列双端队列
散列表
散列表
二叉树二叉搜索树AVL树红黑树字典树哈夫曼树线段树笛卡尔树
图的介绍图的遍历Dijkstra算法Bellman-Ford算法SPFA算法Floyd算法Prim算法Kruskal算法Boruvka算法拓扑排序

信奥赛编程(刷题请进)>>>

经过两年的打磨,我的新作《算法秘籍》已经出版,有需要的可以点击购买。也可以点击 内容介绍 查看详情。