专栏名称: 算法与数据结构
算法与数据结构知识、资源分享
目录
相关文章推荐
算法与数学之美  ·  又一位!著名数学家,在美近40年后,回到中国! ·  12 小时前  
九章算法  ·  「九点热评」亚麻开始用AI打击员工! ·  昨天  
九章算法  ·  TikTok员工精神问题上热搜!疯狂pip背 ... ·  4 天前  
九章算法  ·  2025年BQ面试通关讲座今日开播!FAAN ... ·  2 天前  
51好读  ›  专栏  ›  算法与数据结构

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

算法与数据结构  · 公众号  · 算法  · 2018-07-18 09:46

正文

来自:我就是马云飞(微信号:coding_ma)


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

概述

TreeMap是红黑树的java实现,红黑树能保证增、删、查等基本操作的时间复杂度为O(lgN)。
首先我们来看一张TreeMap的继承体系图:

image

还是比较直观的,这里来简单说一下继承体系中不常见的接口NavigableMap和SortedMap,这两个接口见名知意。先说NavigableMap接口,NavigableMap接口声明了一些列具有导航功能的方法,比如:
/**
 * 返回红黑树中最小键所对应的 Entry
 */

Map.Entry firstEntry();

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

lowerKey(K key);

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

higherKey(K key);

// 其他略

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

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

SortedMap headMap(K toKey);();

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

SortedMap subMap(K fromKey, K toKey);

// 其他略

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

源码分析

添加

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

  • 节点是红色或黑色。

  • 根节点是黑色。

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

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

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

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

    public V put(K key, V value) {
        TreeMapEntry 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 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 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 x) {
        x.color = RED;

        while (x != null && x != root && x.parent.color == RED) {
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                TreeMapEntry 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 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 p) {
    if (p != null) {
        // 取出p的右儿子
        Entry 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 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染为红色,如下所示:






请到「今天看啥」查看全文