查看原文
其他

红黑树真的没你想的那么难!

爱上珊瑚海 CSDN 2018-12-16

写本文的原由是昨晚做梦居然梦到了在看源码,于是便有了此文......

虽然标题是关于红黑树的,不过此文是结合图片,通过分析TreeMap的源码,让你理解起来不是那么枯燥(前方高能,此文图片众多,慎入)。

作者 | 马云飞

责编 | 胡巍巍


概述


TreeMap是红黑树的Java实现,红黑树能保证增、删、查等基本操作的时间复杂度为O(lgn)。    

首先我们来看一张TreeMap的继承体系图:

image

还是比较直观的,这里来简单说一下继承体系中不常见的接口NavigableMap和SortedMap,这两个接口见名知意。先说NavigableMap接口,NavigableMap接口声明了一些列具有导航功能的方法,比如:

/**
 * 返回红黑树中最小键所对应的 Entry
 */

Map.Entry<K,V> firstEntry();

/**
 * 返回最大的键 maxKey,且 maxKey 仅小于参数 key
 */

K lowerKey(K key);

/**
 * 返回最小的键 minKey,且 minKey 仅大于参数 key
 */

K higherKey(K key);

// 其他略

通过这些导航方法,我们可以快速定位到目标的Key或Entry。至于 SortedMap接口,这个接口提供了一些基于有序键的操作,比如:

/**
 * 返回包含键值在 [minKey, toKey) 范围内的 Map
 */

SortedMap<K,V> headMap(K toKey);();

/**
 * 返回包含键值在 [fromKey, toKey) 范围内的 Map
 */

SortedMap<K,V> subMap(K fromKey, K toKey);

// 其他略

以上就是两个接口的介绍,很简单。关于TreeMap的继承体系就这里就说到这,接下来我们深入源码进行分析。


源码分析



添加

红黑树最复杂的无非就是增删了,这边我们先介绍增加一个元素,了解红黑树的都知道,当往 TreeMap 中放入新的键值对后,可能会破坏红黑树的性质。首先我们先巩固一下红黑树的特性。

  • 节点是红色或黑色。

  • 根节点是黑色。

  • 每个叶子节点都是黑色的空节点(NIL节点)。

  • 每个红色节点的两个子节点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色节点)。

  • 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

接下来我们看看添加到底做了什么处理:

  public V put(K key, V value) {
        TreeMapEntry<K,V> t = root;
        if (t == null) {

            if (comparator != null) {
                if (key == null) {
                    comparator.compare(key, key);
                }
            } else {
                if (key == null) {
                    throw new NullPointerException("key == null");
                } else if (!(key instanceof Comparable)) {
                    throw new ClassCastException(
                            "Cannot cast" + key.getClass().getName() + " to Comparable.");
                }
            }
            root = new TreeMapEntry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        TreeMapEntry<K,V> parent;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        TreeMapEntry<K,V> e = new TreeMapEntry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

这边会先把根节点暂存依赖,如果根节点为null,则讲新增的这个节点设为根节点。

如果初始化的时候指定了Comparator比较器,则讲其插入到指定位置,否则使用key进行比较并且插入。

不断地进行比较,找到没有子节点的节点,将其插入到相应节点(注:如果查找出有相同的值只会更新当前值,CMP小于0是没有左节点,反之没有右节点)

新插入的树破环的红黑树规则,我们会通过fixAfterInsertion去进行相应的调整,这也是TreeMap插入实现的重点,具体我们看看他是怎么通过Java实现的。

 private void fixAfterInsertion(TreeMapEntry<K,V> x) {
        x.color = RED;

        while (x != null && x != root && x.parent.color == RED) {
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                TreeMapEntry<K,V> 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));
                } else {
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                TreeMapEntry<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;
    }

首先将新插入的节点设置为红色,这边做了一个判断,新节点不为null,新节点不为根节点并且新节点的父节点为红色。才会进入内部的判断,因为其本身就是一个红黑树。

如果新节点的父节点为黑色,则他依旧满足红黑树的特性。所以当其父节点为红色进入内部的判断。

如果新节点是其祖父节点的左子孙,则拿到其祖父节点的右儿子,也就是新节点的叔叔。如果叔叔节点是红色。

则将其父节点设为黑色,讲叔父节点设为黑色,然后讲新节点直接其祖父节点。

否则如果新节点是其父节点的右节点,以其父节点进行左转,将父节点设为黑色,祖父节点设为红色,在通过祖父节点进行右转。

else内容和上述基本一致。可以自己分析。最后我们还需要将跟节点设为黑色。

我们稍微看一下,他是怎么进行左转和右转的。

// 右旋与左旋思路一致,只分析其一
// 结果相当于把p和p的儿子调换了
private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        // 取出p的右儿子
        Entry<K,V> r = p.right;
        // 然后将p的右儿子的左儿子,也就是p的左孙子变成p的右儿子
        p.right = r.left;
        if (r.left != null)
            // p的左孙子的父亲现在是p
            r.left.parent = p;

        // 然后把p的父亲,设置为p右儿子的父亲
        r.parent = p.parent;
        // 这说明p原来是root节点
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        r.left = p;
        p.parent = r;
    }
}

//和左旋类似
private void rotateRight(Entry<K,V> p) {
    // ...
}

下面我们通过图解来看看如何插入一颗红黑树。

现有数组int[] a = {1, 10, 9, 2, 3, 8, 7, 4, 5, 6};我们要将其变为一棵红黑树。

首先插入1,此时树是空的,1就是根结点,根结点是黑色的:

然后插入元素10,此时依然符合规则,结果如下:

当插入元素9时,这时是需要调整的第一种情况,结果如下:


红黑树规则4中强调不能有两个相邻的红色结点,所以此时我们需要对其进行调整。

调整的原则有多个相关因素,这里的情况是,父结点10是其祖父结点1(父结点的父结点)的右孩子,当前结点9是其父结点10的左孩子,且没有叔叔结点(父结点的兄弟结点),此时需要进行两次旋转,第一次,以父结点10右旋:


然后将父结点(此时是9)染为黑色,祖父结点1染为红色,如下所示:


然后以祖父结点1左旋:


下一步,插入元素2,结果如下:


此时情况与上一步类似,区别在于父结点1是祖父结点9的左孩子,当前结点2是父结点的右孩子,且叔叔结点10是红色的。

这时需要先将叔叔结点10染为黑色,再进行下一步操作,具体做法是将父结点1和叔叔结点10染为黑色,祖父结点9染为红色,如下所示:


由于结点9是根节点,必须为黑色,将它染为黑色即可:


下一步,插入元素3,如下所示:


这和我们之前插入元素10的情况一模一样,需要将父结点2染为黑色,祖父结点1染为红色,如下所示:


然后左旋:


下一步,插入元素8,结果如下:

此时和插入元素2有些类似,区别在于父结点3是右孩子,当前结点8也是右孩子,这时也需要先将叔叔结点1染为黑色,具体操作是先将1和3染为黑色,再将祖父结点2染为红色,如下所示:


此时树已经平衡了,不需要再进行其他操作了,现在插入元素7,如下所示:


这时和之前插入元素9时一模一样了,先将7和8右旋,如下所示:


然后将7染为黑色,3染为红色,再进行左旋,结果如下:


下一步要插入的元素是4,结果如下:


这里和插入元素2是类似的,先将3和8染为黑色,7染为红色,如下所示:


但此时2和7相邻且颜色均为红色,我们需要对它们继续进行调整。这时情况变为了父结点2为红色,叔叔结点10为黑色,且2为左孩子,7为右孩子,这时需要以2左旋。

这时左旋与之前不同的地方在于结点7旋转完成后将有三个孩子,结果类似于下图:


这种情况处理起来也很简单,只需要把7原来的左孩子3,变成2的右孩子即可,结果如下:


然后再把2的父结点7染为黑色,祖父结点9染为红色。结果如下所示:



此时又需要右旋了,我们要以9右旋,右旋完成后7又有三个孩子,这种情况和上述是对称的,我们把7原有的右孩子8,变成9的左孩子即可,如下所示:


下一个要插入的元素是5,插入后如下所示:


有了上述一些操作,处理5变得十分简单,将3染为红色,4染为黑色,然后左旋,结果如下所示:



最后插入元素6,如下所示:


又是叔叔结点3为红色的情况,这种情况我们处理过多次了,首先将3和5染为黑色,4染为红色,结果如下:

此时问题向上传递到了元素4,我们看2、4、7、9的颜色和位置关系,这种情况我们也处理过,先将2和9染为黑色,7染为红色,结果如下:


最后7是根结点,染为黑色即可,最终结果如下所示:


可以看到,在插入元素时,叔叔结点是主要影响因素,待插入结点与父结点的关系决定了是否需要多次旋转。


删除


除了添加操作,红黑树的删除也是很麻烦的…...我们看看怎么通过Java去实现红黑树的删除。具体代码如下:

public V remove(Object key) {
        TreeMapEntry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }

其内部是通过Delete Entry去进行删除的。所以我们具体看看Delete Entry的实现。

 private void deleteEntry(TreeMapEntry<K,V> p) {
        modCount++;
        size--;

        if (p.left != null && p.right != null) {
            TreeMapEntry<K,V> s = successor(p);
            p.key = s.key;
            p.value = s.value;
            p = s;
        } 

        TreeMapEntry<K,V> replacement = (p.left != null ? p.left : p.right);

        if (replacement != null) {
            // Link replacement to parent
            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;

            // Fix replacement
            if (p.color == BLACK)
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { 
            root = null;
        } else {
            if (p.color == BLACK)
                fixAfterDeletion(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指向其后继节点。 

然后将 Replacement Parent引用指向新的父节点,同时让新的父节点指向 Replacement。

然后判断如果删除的节点P是黑色节点,则需要进行调整。删除的是根结点并且树中只有一个节点,我们将根结点置为null,否则,如果删除的节点没有子节点并且是黑色,则需要调整。最后将p从树中移除。

删除了一个元素,为了保证还是一个红黑树,我们需要将其进行调整,具体代码如下:

  /** From CLR */
    private void fixAfterDeletion(TreeMapEntry<K,V> x) {
        while (x != root && colorOf(x) == BLACK) {
            if (x == leftOf(parentOf(x))) {
                TreeMapEntry<K,V> sib = rightOf(parentOf(x));

                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 {
                    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 { // symmetric
                TreeMapEntry<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);
}

如果替换节点是父节点的左节点,并且替换节点的兄弟节点是红色,那我们需要将兄弟节点变成黑色,将父节点变成红色,并且通过父节点进行左旋转,然后将父节点的右节点设为兄弟节点。

如果兄弟节点的左右节点都是黑色的,那么将兄弟节点置为红色,并且将当前节点指向父节点。

若兄弟节点的右节点是黑色,我们需要将兄弟节点的左节点设为黑色,将兄弟节点设为红色,然后以兄弟节点进行右旋转,然后更新兄弟节点。

然后设置兄弟节点的颜色为右节点的颜色,然后将父节点和兄弟节点的左节点设为黑色,最后进行右旋转。最后将根结点设为黑色。

下面我们依旧通过图解来看看红黑树的删除操作:要从一棵红黑树中删除一个元素,主要分为三种情况。

待删除元素没有孩子

没有孩子指的是没有值不为NIL的孩子。这种情况下,如果删除的元素是红色的,可以直接删除,如果删除的元素是黑色的,就需要进行调整了。

例如我们从下图中删除元素1:


删除元素1后,2的左孩子为NIL,这条支路上的黑色结点数就比其他支路少了,所以需要进行调整。

这时,我们的关注点从叔叔结点转到兄弟结点,也就是结点4,此时4是红色的,就把它染为黑色,把父结点2染为红色,如下所示:


然后以2左旋,结果如下:


此时兄弟结点为3,且它没有红色的孩子,这时只需要把它染为红色,父结点2染为黑色即可。结果如下所示:

待删除元素有一个孩子

这应该是删除操作中最简单的一种情况了,根据红黑树的定义,我们可以推测,如果一个元素仅有一个孩子,那么这个元素一定是黑色的,而且其孩子是红色的。

假设我们有一个红色节点,它是树中的某一个节点,且仅有一个孩子,那么根据红色节点不能相邻的条件,它的孩子一定是黑色的,如下所示:

但这个子树的黑高却不再平衡了(注意每个节点的叶节点都是一个NIL节点),因此红色节点不可能只有一个孩子。

而若是一个黑色节点仅有一个孩子,如果其孩子是黑色的,同样会打破黑高的平衡,所以其孩子只能是红色的,如下所示:


只有这一种情况符合红黑树的定义,这时要删除这个元素,只需要使用其孩子代替它,仅代替值而不代替颜色即可,上图的情况删除完后变为:


可以看到,树的黑高并没有发生变化,因此也不需要进行调整。

待删除元素有两个孩子

我们知道如果删除一个有两个孩子的元素,可以使用它的前驱或者后继结点代替它。因为它的前驱或者后继结点最多只会有一个孩子,所以这种情况可以转为上述两种情况进行处理。


查找


说了最复杂的添加和删除,我们再来看看查找,这里就简单很多了,具体代码如下:

public V get(Object key) {
    Entry<K,V> p = getEntry(key);
    return (p==null ? null : p.value);
}

final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;

    // 查找操作的核心逻辑就在这个 while 循环里
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

这个流程算比较简单了,上面注释标明了,这边就不再解释了。


总结


这边通过图+代码的形式将红黑树的特点展现出来。可以通过上面描述可见,红黑树并没有那么难…

此文参考资料:

  • https://www.jianshu.com/p/3958a1a11cb0(图片均来自此博客)

作者简介:马云飞,程序猿,业余时间看看开源,扯扯技术,写写博文,有个人微信公众号「码农职场」,如若喜欢不妨关注看看吧。

推荐阅读:

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存