结合 TreeMap 源码分析红黑树在 java 中的实现

csdn 连接:blog.csdn.net/ziwang_/art…java

注:本文的源码摘自 jdk1.8 中 TreeMap数组

红黑树的意义


红黑树本质上是一种特殊的二叉查找树,红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(lgN)。那么红黑树是如何实现这个特性的呢?红黑树区别于其余二叉查找树的规则在于它的每一个结点拥有红色或黑色中的一种颜色,而后按照必定的规则组成红黑树,而这个规则就是咱们这篇文章所想要阐述的了。

红黑树的性质


红黑树遵循如下五点性质:

  • 性质1 结点是红色或黑色。
  • 性质2 根结点是黑色。
  • 性质3 每一个叶子结点(NIL结点,空结点)是黑色的。
  • 性质4 每一个红色结点的两个子结点都是黑色。(从每一个叶子到根的全部路径上不能有两个连续的红色结点)
  • 性质5 从任一结点到其每一个叶子结点的全部路径都包含相同数目的黑色结点。

如下有几个违反上述规则的结点示例:bash

违反性质1
违反性质1

结点必须是红色或黑色学习

违反性质2
违反性质2

根结点必须是黑色的ui

违反性质3
违反性质3

叶子结点必须是黑色的spa

违反性质4
违反性质4

违反性质4
违反性质4

违反性质4
违反性质4

以上三个都是错误的红黑树示例,每一个红色结点的两个子结点都是黑色,而以下是合格的.net

遵循性质4
遵循性质4

固然,细心的读者应该发现了我只是展现了前四条性质而没有展现第五条性质,没有什么理由,笔者就是懒,第五条挺好理解的。翻译

左旋、右旋


在学习红黑树以前想要介绍一个概念——左旋、右旋。这是一种结点操做,是红黑树里面时常出现的一个操做,请看下图 ——

左旋右旋概念图
左旋右旋概念图

这里的左旋右旋都是针对根节点而言的,因此左图到右图是 y 结点右旋,右图到左图是 x 结点左旋。设计

  • 左旋:根结点退居右位,左子结点上位,同时左子结点的右子结点变成根节点左结点。
  • 右旋:根节点退居左位,右子节点上位,同时右子结点的左子结点变成根节点右结点。

如今不理解这俩概念有什么用不重要,可是但愿读者能理解它的变幻过程,到后面会涉及到。3d

提及来枯燥无心,咱们能够结合 TreeMap 来看看左旋右旋的源码 ——

方法图
方法图

在这里咱们就针对左旋源码看看 ——

左旋源码
左旋源码

笔者就直接一行一行解释吧:

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;         // r 是根结点右子结点
        p.right = r.left;               // 为根结点的左结点指向右子结点(也就是 r)的左结点
        if (r.left != null)
            r.left.parent = p;          // 意义同第二步,这步是右子结点(也就是 r)的左结点将父结点引用指向 p
        r.parent = p.parent;            // 将 r 结点的父引用指向 p 结点的父引用
        if (p.parent == null)
            root = r;                   // 将根结点替换为 r
        else if (p.parent.left == p)
            p.parent.left = r;          // 意义同上
        else
            p.parent.right = r;         // 意义同上
        r.left = p;                     // r 左结点引用指向 p 结点
        p.parent = r;                   // p 结点父引用指向 r 结点
    }
}复制代码


假设如今咱们找到了相应的结点插入位置,那么咱们接下来就能够插入相应的结点了,这个时候迎来一个头疼的问题,咱们知道红黑树结点是有颜色的,那么咱们应该给它设置成黑色的仍是红色的呢?

设置成黑色的吧,就违反了性质5,设置成了红色的吧,就容易违反了性质4。那怎么办?总要给一个颜色,那咱们就给红色的吧。为何?由于若是设置成黑色的话,该分支的黑色结点数量确定比其余分支多一个,而这样的话至关地很差作调整。若是将插入结点颜色置为红色的话,运气比较好的状况下该父结点就是黑色的,那这样就不须要作任何调整。另外一种状况是插入结点的父结点颜色是红色的,这种状况咱们就须要详细讨论了,具体分为如下两种(此处咱们以插入结点的父结点是爷爷结点的左子结点为例(有点拗口),镜像操做道理相同):

  • 1.父结点与叔叔结点都为红

父结点与叔叔结点都为红
父结点与叔叔结点都为红

父结点与叔叔结点都为红的话那么一定爷爷结点为黑,实际上此时咱们最简单的操做就是将父结点和叔叔结点染黑,将爷爷结点染红(将爷爷结点染红的目的是为了保证爷爷结点路径的黑色结点数量不改变),以下 ——

染黑
染黑

如今目标结点、父结点、叔叔结点都符合要求了,可是爷爷结点的父结点是红色的,那么就冲突了,聪明的读者可能已经发现了,此时的爷爷结点就至关于目标结点,咱们不妨将爷爷结点置换为目标结点,再进行递归操做就能够达到解决冲突的目的了。

  • 2.父结点为红,叔叔结点为黑

父结点为红,叔叔结点为黑
父结点为红,叔叔结点为黑

但凡是有一个结点是红色,那么它的父结点一定是黑色(性质4),因此爷爷结点必定是黑色的。

有细心的小伙伴可能觉察到,上图违反了性质五。实际上上图是一张简化后的图,为了咱们后面的内容更加便于理解,上图的原图应该是如下模样 ——

上图原图
上图原图

ps:上图中叔叔结点和兄弟结点能够理解成 java 中的 null 结点,笔者特意将它们的个头缩小了,以便区分。

那么此时该怎么操做呢?爷爷结点右旋,爷爷结点置红,父结点置黑。这条操做事后,性质四、5都没有违反。

爷爷结点右旋,爷爷结点置红,父结点置黑
爷爷结点右旋,爷爷结点置红,父结点置黑

固然,上图也只是一张简化图,实际上原图以下:

上图原图
上图原图

那么结合 TreeMap 源码咱们来看看:

插入调整源码
插入调整源码

翻译以下:

private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;      // 目标结点颜色赋红

    // 目标结点非空,非根,同时父结点为红,此时才须要调整
    while (x != null && x != root && x.parent.color == RED) {
        // 父结点是爷爷的左子结点
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));  // y 是叔叔结点
            // 状况1 叔叔结点也为红
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);               // 父结点赋黑
                setColor(y, BLACK);                         // 叔叔结点赋黑
                setColor(parentOf(parentOf(x)), RED);       // 爷爷结点赋红
                x = parentOf(parentOf(x));                  // 爷爷结点置为目标结点,递归
            } else {
                // 状况2 叔叔结点为黑
                // 小插曲,若是目标结点是父结点的右子结点,左旋父结点
                // 固然,此时目标结点应改成父结点
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                setColor(parentOf(x), BLACK);               // 父结点赋黑
                setColor(parentOf(parentOf(x)), RED);       // 爷爷结点赋红
                rotateRight(parentOf(parentOf(x)));         // 爷爷结点右旋
            }
        } else {
            // 镜像操做,道理同上
            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;     // 根结点必须赋黑
}复制代码

看完代码咱们发现咱们好像漏了一个小插曲(固然,这是笔者故意的),那么小插曲是一个什么状况呢?言语来讲,在叔叔结点为黑的前提下,当目标结点是父结点的右子结点的时候,须要对父结点进行左旋而后才能接续下一步操做,为何会这样,咱们一图胜千言 ——

小插曲
小插曲

若是忽略上述状况,那么最终会获得如下状况:

小插曲忽略状况下实现
小插曲忽略状况下实现

因为目标结点是父结点的右子节点,在爷爷结点右旋过程当中,它会转为原爷爷结点的左子结点,这样的话就违反了特性4和特性5。解决方法就是上面所提到的将父结点先进行左旋而后再进行前面所提到的操做,以下图 ——

小插曲修正
小插曲修正

固然,不要忘了,如今须要调整的结点是原父结点,也就是要将上图左下角那个结点做为目标结点进行调整。

因此红黑树的添操做分为如下三步:

  • 找到相应的插入位置
  • 将目标结点设置为红色并插入
  • 经过着色和旋转等操做使之从新成为一棵二叉树


这一小节我想先 show 出源码再来解释 ——

删除结点源码
删除结点源码

翻译以下:

private void deleteEntry(Entry<K,V> p) {
    // 优先选择左子结点做为被删结点的替代结点
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

    // 若是替代结点不为空
    if (replacement != null) {
        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;

        // 将删除结点的各个引用置 null
        p.left = p.right = p.parent = null;

        // 若是删除结点颜色为黑色,那么须要进行删后调整
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {
        // 若是替代结点为空且删除结点为 root 结点
        root = null;
    } else {
        // 若是删除结点为空且不是 root 结点
        // 若是删除结点颜色为黑色,那么须要进行删后调整
        if (p.color == BLACK)
            fixAfterDeletion(p);

        // 将删除结点的各个引用置 null
        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;
        }
    }
}复制代码

删除时可能分为三种状况,具体的作法也在上述代码中作了清晰的解释,笔者在此就不扩展了,细心的读者可能发现了,上述删除操做凡是涉及到了删除结点是黑色的状况下,都须要调用 fixAfterDeletion() 方法对红黑树进行调整。这是由于若是删除结点是黑色的,当它被删除后就会违反性质5,因此咱们须要对红黑树进行结构调整。

为了便于理解红色结点为何不会影响红黑树总体结构,笔者仍是举了一个例子给各位读者理解一下,下图是删除前:

删除前
删除前

下图是删除后:

删除后
删除后

实际上红黑树是使用如下2点思想来进行调整的(笔者认为,在分析 fixAfterDeletion() 代码实现以前,做为开发者应该去自行思考一下若是咱们做为源码设计者,咱们会如何来解决这个问题。) ——

1.给删除结点的路径增长一个黑色结点(将兄弟路径的一个黑色结点移过来)
2.给删除结点的兄弟路径减小一个黑色结点(将兄弟路径的一个红色结点染黑)

ps:后面咱们会针对第一条称为思想1,第二条称为思想2

说完思想,咱们讨论一下具体删除操做是如何进行的。红黑树在保障删除结点的兄弟结点为黑色的状况下(没有什么特殊原因,仅仅是为了后期好操做),分如下两点来进行分析:

1.兄弟结点的两个子结点都是黑色的
2.另外一种状况(兄弟结点的两个子结点至多一个黑色的)

ps:后面咱们会针对第一条称为状况1,第二条称为状况2

对于状况1来讲,红黑树采用思想2,将兄弟结点置为红色,可是这样带来了两个问题——对于父路径来讲,它与兄弟路径黑色结点数量不一样,违反性质5;且若是父结点也是红色,那么它势必与孩子结点冲突,还会违反性质4,以下图——

下图示例违反性质5:

原图
原图

违反性质5
违反性质5

下图示例违反性质5且违反性质4:

原图
原图

违反性质四、5
违反性质四、5

对于前一个问题用递归的思想来解决,将父亲结点置为目标结点,让父亲结点的兄弟结点也要减小一个黑色结点就能够了(借鉴思想2);而对于后一个问题,只须要将父结点置黑便可(借鉴思想2)。jdk 中相关实现源码以下:

while (x != root && colorOf(x) == BLACK) {
    Entry<K,V> sib = rightOf(parentOf(x));
    if (colorOf(leftOf(sib))  == BLACK &&
        colorOf(rightOf(sib)) == BLACK) {
        setColor(sib, RED);
        x = parentOf(x);
    }
}

setColor(x, BLACK);复制代码

前面阐述的是针对状况1而言,针对于状况2而言,红黑树采用的是思想1,具体作法分为又得分为如下两种小状况:

  • 兄弟结点的右子结点不为黑
  • 兄弟结点的右子结点为黑

对于第一种小状况,红黑树采用如下操做:

1.兄弟结点置父结点颜色(准备谋权篡位)
2.父结点置黑、兄弟结点右结点置黑
3.父结点左旋

该思想不只保证了更新结点后不会冲突(父结点与兄弟结点不冲突,兄弟结点与右子结点不冲突,兄弟结点左子结点与父结点不冲突),而且保证了黑色结点数量不会改变,一图胜千言——

第一种小状况原图
第一种小状况原图

第一种小状况删除后修正
第一种小状况删除后修正

jdk 中相关源码以下:

while (x != root && colorOf(x) == BLACK) {
    setColor(sib, colorOf(parentOf(x)));
    setColor(parentOf(x), BLACK);
    setColor(rightOf(sib), BLACK);
    rotateLeft(parentOf(x));
    x = root;
}

setColor(x, BLACK);复制代码

而对于第二种小状况,红黑树采用如下操做:

1.将兄弟结点的左子结点染黑
2.兄弟结点染红
3.兄弟结点右旋

第二种小状况原图
第二种小状况原图

第二种小状况删除后修正
第二种小状况删除后修正

实际上细心的读者发现了,转换后的结构是等同于第一种小状况的初始结构,因此接下来就按照第一种小状况的步骤去变换结构,相关源码以下:

while (x != root && colorOf(x) == BLACK) {
    if (colorOf(rightOf(sib)) == BLACK) {   // 状况2
        setColor(leftOf(sib), BLACK);
        setColor(sib, RED);
        rotateRight(sib);
        sib = rightOf(parentOf(x));
    }

    // 状况1
    setColor(sib, colorOf(parentOf(x)));
    setColor(parentOf(x), BLACK);
    setColor(rightOf(sib), BLACK);
    rotateLeft(parentOf(x));
    x = root;
}

setColor(x, BLACK);复制代码

这一块可能有一些复杂,但记住如下三点核心思想问题就不是很大了:

  • 父结点替换删除结点(保障了删除结点路径上的黑色结点数量不变)
  • 兄弟结点替换父结点(保障了父结点路径上的黑色结点数量不变)
  • 右子结点(结构变化前必定是红色的,变换后置黑)替换兄弟结点(保障了兄弟路径上的黑色结点数量不变)

那么接下来就是看看 fixAfterDeletion() 的代码实现了 ——

结点删除调整源码
结点删除调整源码

解释以下:

private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        // 目标结点是左子结点
        if (x == leftOf(parentOf(x))) {
            // 目标结点的兄弟结点
            Entry<K,V> sib = rightOf(parentOf(x));

            // 小插曲1,若是兄弟结点为红
            // 这步是保障兄弟结点必定为黑
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);           // 兄弟结点置黑
                setColor(parentOf(x), RED);     // 父结点置红
                rotateLeft(parentOf(x));        // 父结点左旋
                sib = rightOf(parentOf(x));     // 重定向兄弟结点
            }

            // 兄弟结点的两个子结点是黑色
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);             // 兄弟结点置红
                x = parentOf(x);                // 重定向目标结点为父结点
            } else {
                // 兄弟结点的子结点至多一个是黑色的

                // 小插曲2,兄弟结点左子结点为红,右子结点为黑的状况
                // 这步的意义是让兄弟结点的右子结点的数量多一个
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                // 将兄弟结点颜色置为父结点颜色(言外之意确定是兄弟结点要替换父结点的位置)
                setColor(sib, colorOf(parentOf(x)));
                // 将父结点置黑
                setColor(parentOf(x), BLACK);
                // 将兄弟结点右子结点置黑
                setColor(rightOf(sib), BLACK);
                // 左旋父结点
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // 镜像操做
            Entry<K,V> sib = leftOf(parentOf(x));

            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }

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

    setColor(x, BLACK);
}复制代码

总结


红黑树的插入操做是基于插入结点颜色为红色,缘由是若是插入结点是黑色的话,会致使涉及到该结点的路径上的黑色结点数量会比兄弟路径的黑色结点数量多一个,那么总体调节起来势必很不方便。而删除操做是基于删除结点若是是黑色的状况下,才须要进行调整,由于黑色结点的删除会致使涉及到该结点的路径上的黑色结点数量会比兄弟路径的黑色结点数量少一个,那么就须要进行总体调节。

红黑树在 java 中的运用实际上仍是挺多的,例如 TreeSet 的默认底层实现实际上也是 TreeMap;jdk 8中的 HashMap 实现也由原来的数组+链表更改成了数组+链表/红黑树。

相关文章
相关标签/搜索