• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

红黑树、TreeMap、TreeSet

开发技术 开发技术 2个月前 (12-01) 31次浏览

事先声明以下代码基于JDK1.8版本

参考资料

大部分图片引自https://www.jianshu.com/p/e136ec79235c侵删

https://www.cnblogs.com/skywang12345/p/3245399.html#!comments

https://www.cnblogs.com/CarpenterLee/p/5525688.html

https://www.cnblogs.com/CarpenterLee/p/5503882.html

TreeMap用法

TreeMap这样一种数据结构给了我们什么:

  1. map一样的键值对结构存储
  2. TreeMap间接继承自SortedMap,元素有序,有序的tree也意味着这是一颗二叉搜索树,因此它的查询是log(n)级别的
  3. 不允许null的key
  4. 不允许重复的key
  5. 并不同步,如果需要同步,可以利用Collections包装一下Collections.synchronizedSortedMap()

如果你有上述的需求,其实也就是快速搜索,可以考虑使用它

红黑树

为什么是红黑树?二叉搜索树?AVL?

TreeMap是通过什么样的数据结构来存储数据的呢?红黑树

  • 首先TreeMap要求有序,所以它至少是一颗二叉搜索树

那为什么二叉搜索树又不行呢?

在一定的情况下,二叉搜索树会退化成链表,失去了log(n)的搜索时间复杂度

如下图所示,如果按照1,2,3,4这样的顺序去构成二叉搜索树的话,它就会退化成一个链表,这样的性能是没法接收的

红黑树、TreeMap、TreeSet

那么AVL树又如何呢?为什么就选择红黑树呢

红黑树由于其特性,它并不追求完全的平衡,更低的平衡要求对应着插入、删除效率的提高(可能提高并不明显甚至不如AVL,因为插入、删除的第一步就是定位),搜索的效率降低,AVL相对红黑树而言,要求完全的平衡,自然插入、删除需要进行的操作更多,效率更低,而由于完全的平衡,搜索的效率就更高,综合而言,选择了实现起来更为简单,各方面更为均衡的红黑树,就是如此

红黑树的性质

  1. 任何一个节点不是红色就是黑色
  2. 根节点是黑色
  3. 任何两个红色节点不能直接相连
  4. 每个叶子节点(Nil)节点都为黑色,也就是null节点为黑色
  5. 从根节点出发,到任意一个叶子节点的简单路径(不包含叶子节点)的黑色节点数目要相同

推论

  1. 如果一个节点存在黑色子节点,那么它一定有两个子节点,为了满足性质5

左旋与右旋与着色

红黑树为了维持上述5个性质,需要做出一些努力,这些努力就是旋转着色

左旋示意图:

红黑树、TreeMap、TreeSet

语言总结一下就是:

左旋,右孩子成为旋转中心的父亲节点,旋转中心成为右孩子的左孩子,此时旋转中心的右孩子空缺,右孩子的左子树失去了父亲,把右孩子的左子树置为旋转中心的右孩子

对应的TreeMap的rotateLeft方法如下

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;
        //p的右孩子->r的左子树
        p.right = r.left;
        if (r.left != null)
            //右孩子指向p.parent
            r.left.parent = p;
        r.parent = p.parent;
        //判断p是否为root以及p在parent中的位置
        if (p.parent == null)
            root = r;
        else if (p.parent.left == p)
            p.parent.left = r;
        else
            p.parent.right = r;
        //p成为r的左孩子,r成为p的父亲
        r.left = p;
        p.parent = r;
    }
}

右旋示意图

红黑树、TreeMap、TreeSet

TreeMap中的右旋代码

private void rotateRight(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> 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;
    }
}

着色

Entry的color成员变量如下

boolean color = BLACK;

//上色方法如下
private static <K,V> void setColor(Entry<K,V> p, boolean c) {
    if (p != null)
        p.color = c;
}

红黑树的查找

红黑树本质还是一颗二叉搜索树,自然与二叉搜索树的查找方式相差无几

TreeMap中的实现如下:

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 (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;
}

插入过程如何保持平衡?

插入过程的步骤
  • 利用已有的红黑树结构找到插入位置
  • 插入一个新的红色节点
  • 调整
为啥新插入的节点要是红色呢?

由于性质5的存在,插入节点黑色势必导致红黑树的性质5的不满足,而插入红色并不一定需要调整树的结构

插入情景分类

1-3
  1. root为null,直接插入并且,着色为black即可
  2. 插入节点的key已经存在,在TreeMap中,找到对应的节点直接修改value
  3. 插入节点的父亲节点为黑色节点,直接插入即可,不会违背任何一条性质
  4. 插入节点的父亲节点为红色节点
    1. 父亲为红色,并且当且节点的祖父节点的另一个子结点即叔叔节点也是红色
    2. 父亲为红色,叔叔节点为黑色(包括Nil),其父亲为祖父节点的左孩子
      1. 插入节点为父亲节点的左孩子
      2. 插入节点为父亲节点的右孩子
    3. 父亲为红色,叔叔节点为黑色(包括Nil),其父亲为祖父节点的右孩子
      1. 插入节点为父亲节点的左孩子
      2. 插入节点为父亲节点的右孩子

4.2与4.3完全对称式的操作,以4.2为例

4.1父亲为红色,叔叔为红色

操作如下

  • 父亲与叔叔节点置为黑色
  • 祖父节点置为红色
  • 把祖父节点当成新插入的节点,向上递归

为什么如此操作?

当前节点为红,父亲为红,违背性质4,修改父亲为黑,此时经过父亲节点的路径的黑色节点数量大于经过叔叔节点的路径黑色节点数目,那就再把叔叔染黑,那么此时父辈平衡,但是经过祖父的这条分支黑色节点数目多了一个,那么再把祖父染红即可,然后以祖父为新插入的节点再插入即可

效果如图:

红黑树、TreeMap、TreeSet

4.2.1 父亲为红色,叔叔为黑色(Nil),父亲为祖父节点的左孩子,插入节点为父亲节点的左孩子

操作如下

  • 将父亲置为黑色
  • 将祖父置为红色
  • 以祖父为中心,右旋

效果如图:

红黑树、TreeMap、TreeSet

为什么这么操作?

不满足性质4,修改父亲为黑色,此时父亲这边的路径黑色数量多1,那么再修改祖父为红色,但此时经过叔叔的路径黑色数量少1,此时以祖父为中心,右旋,让刚刚染色为黑色的节点成为父亲,父亲成为叔叔的父亲,即可满足平衡

4.2.2 父亲为红色,叔叔为黑色(Nil),父亲为祖父节点的左孩子,插入节点为父亲节点的右孩子

操作如下:

  • 以父亲节点为中心,左旋
  • 达到4.2.1的效果,然后按照上面的操作来

效果如图:

红黑树、TreeMap、TreeSet

为什么这么操作?

就是为了变成4.2.1,然后按照4.2.1的操作进行即可

4.3是4.2的镜像版本,对称过去就好了

TreeMap中的插入代码

put方法
public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check
		//1 如果root为null,插入并返回即可
        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    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
                //2 如果找到了,直接修改value
                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);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    //3 直接插入节点,默认color为黑色
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}
fixAfterInsertion()方法
private void fixAfterInsertion(Entry<K,V> x) {
    //把color着色为红
    x.color = RED;

    while (x != null && x != root && x.parent.color == RED) {
        //父亲为祖父的左孩子
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            //y-叔叔节点
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                //叔叔为红--4.1,修改父亲和叔叔的节点为黑,祖父为红
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                //将祖父设置为当前节点
                x = parentOf(parentOf(x));
            } else {
                //插入节点为右孩子,则先左旋一下,从4.2.2 --> 4.2.1
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                //4.2.1,修改父亲为黑色,祖父为红色,以祖父为中心右旋
                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)));
            }
        }
    }
    //如果x为root,直接着色即可
    root.color = BLACK;
}

删除过程如何保持平衡?

删除过程分为两步:
  1. 定位到要删除的节点,按照节点分类情况删除
  2. 通过旋转、着色平衡红黑树
节点分类:
  1. 没有孩子节点,直接删除即可
  2. 有一个孩子节点,直接删除该节点,用唯一子节点代替它的位置
  3. 有两个孩子,寻找后继节点(第一个比它大的节点),把后继节点的内容复制过来,删除后继节点,此时的后继节点不可能有两个孩子节点,有的话,后继节点一定不是它,删除后继节点,按照情况1,2处理

后继节点

后继节点的选取,选投影到x轴上离它最近的两个节点(肯定有两个)中的任意一个都可以,这里选择第一个比删除节点大的节点,对应到树中的位置如下

  • 如果删除节点有右子树,则对应着右子树的最左节点

  • 如果没有则对应着第一个向右走的节点,如图所示

    红黑树、TreeMap、TreeSet

    为什么是后继节点?
    1. 后继节点来替换,它大于>待删除节点>待删除节点左子树所有节点,它是第一个大于待删除节点的节点,它小于<右子树的所有节点,即满足基本的二叉搜索树的性质
    2. 只赋值,不修改颜色,即不破坏红黑树的性质,此时只需要考虑删除后继节点的影响即可

    后继节点一定是树末尾的节点,因为任何一个左右孩子非空的节点都不可能成为最终的后继节点,它递归下去最终到树梢的位置

    删除过程对应如下:

    红黑树、TreeMap、TreeSet

现在完成了第一步,定位真正要删除的节点,下面就是删除这个节点之后如何平衡二叉树?

删除情况分类

  1. 删除节点为红色
  2. 删除节点为黑色,且为父亲节点的左孩子
    1. 兄弟节点为红色,(此时父节点,兄弟节点的子节点都为黑色(Nil))
    2. 兄弟节点为黑色,兄弟节点的两个子结点也是黑色(Nil)
    3. 兄弟节点为黑色,兄弟节点的左孩子为红色,右孩子为黑色(Nil)
    4. 兄弟节点为黑色,兄弟节点的右孩子为红色,左孩子任意
  3. 删除节点为黑色,且为父亲节点的右孩子,和上面是对称的
    1. 兄弟节点为红色,(此时父节点,兄弟节点的子节点都为黑色(Nil))
    2. 兄弟节点为黑色,兄弟节点的两个子结点也是黑色(Nil)
    3. 兄弟节点为黑色,兄弟节点的左孩子为红色,右孩子为黑色(Nil)
    4. 兄弟节点为黑色,兄弟节点的右孩子为红色,左孩子任意

只讨论1,2的情况,3与2对称操作

1 删除节点为红色

红色节点死不足惜!!无需处理,删除即可

2.2 删除节点为黑色,且为父亲节点的左孩子,兄弟节点为黑色,兄弟节点的两个子结点都是黑色

操作如下

  • 将兄弟节点置为红色
  • 将删除节点的父亲节点置为新的待删除节点

效果如图:

红黑树、TreeMap、TreeSet

why

假设R删除了,那么为了达到平衡,P应该帮忙补一个黑色回来,那么出问题了,任何包含P和S的路径黑色就多出来1了,把S置为红色即可达到局部的平衡,把需要补黑色的需求向上传递

2.1 删除节点为黑色,且为父亲节点的左孩子,兄弟节点为红色

操作如下:

  • 将兄弟置为黑色
  • 将父亲置为红色
  • 以父亲节点为中心,左旋
  • 左旋之后重新设置兄弟节点

效果如图:

红黑树、TreeMap、TreeSet

why

向2.2/2.3/2.4靠拢

2.4 删除节点为黑色,且为父亲节点的左孩子,兄弟节点为黑色,兄弟节点的右孩子为红色,左孩子任意

操作如下:

  • 父亲节点的颜色赋值给兄弟节点
  • 将父亲节点置为黑色
  • 将兄弟节点的右孩子置为黑色
  • 以父亲节点为中心左旋

效果如图:

红黑树、TreeMap、TreeSet

why

我们的最终目的就是为包含R的路径补一个黑色节点

  1. 修改P为黑色(不一定能保证补回一个黑色,有可能P就是黑色的)

  2. 以P为中心左旋,让S成为包含R路径的新root,那么包含S的路径黑色节点没变,包含R的路径的黑色节点由于S的存在+1

左旋之后,P与SL为父子关系,而P与SL又都为任意颜色,不一定能保证性质4,所以在左旋之前将P与S颜色互换,但是此时包含SR(红色)的右子树黑色节点少1,将SR置为黑色即可

2.3 删除节点为黑色,且为父亲节点的左孩子,兄弟节点为黑色,兄弟节点的左孩子为红色,右孩子为黑色(Nil)

操作如下:

  • 将兄弟节点的左孩子置为黑色
  • 将兄弟节点置为红色
  • 以兄弟节点为中心右旋得到2.4的情况
  • 按照2.4的方法处理

效果如图:

红黑树、TreeMap、TreeSet

删除节点为黑色,且为父亲节点的右孩子节点的操作与上述操作对称

TreeMap中的删除代码

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

        V oldValue = p.value;
        deleteEntry(p);
        return oldValue;
    }
deleteEntry()
private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;

    // If strictly internal, copy successor's element to p and then make p
    // point to successor.
    //left与right都非空,即寻找后继节点
    if (p.left != null && p.right != null) {
        Entry<K,V> s = successor(p);
        //将s的值赋值给p,然后让p指向s
        p.key = s.key;
        p.value = s.value;
        p = s;
    } // p has 2 children

    // Start fixup at replacement node, if it exists.
    //left!=null则为left,right!=null则为right
    Entry<K,V> replacement = (p.left != null ? p.left : p.right);

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

        // Null out links so they are OK to use by fixAfterDeletion.
        //for GC
        p.left = p.right = p.parent = null;

        // Fix replacement
        //如果删除节点为黑色,则调整
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) { // return if we are the only node.
        //唯一节点,root节点
        root = null;
    } else { //  No children. Use self as phantom replacement and unlink.
        //左右孩子均为null
        if (p.color == BLACK)
            fixAfterDeletion(p);
		//断开p与parent的连接
        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;
        }
    }
}
successor()
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {
        //右子树的最左下节点
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {
        //第一个向右走的节点
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}
fixAfterDeletion()
private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        //待删除节点为父亲节点的左孩子且为黑色
        if (x == leftOf(parentOf(x))) {
            //sib--兄弟节点
            Entry<K,V> sib = rightOf(parentOf(x));
			//2.1-->2.2/2.3/2.4
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }
			//2.2 设置兄弟节点为红色,x修改为parent
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                //2.3-->2.4
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                //2.4
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(rightOf(sib), BLACK);
                rotateLeft(parentOf(x));
                x = root;
            }
        } else { // symmetric
            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;
            }
        }
    }
	//如果x为red,直接设置为x,即可弥补回丢失的黑色节点,如果是黑色,没法通过setColor来补回黑色,通过平衡的方式来补回黑色
    setColor(x, BLACK);
}

红黑树总结

总结来说,无论是插入也好,删除也罢,红黑树保持平衡的策略是自底向下追求红黑树的平衡,把矛盾交给上层的节点解决

TreeMap的keySet、EntrySet、values浅析

TreeMap红黑树的代码已经混杂在上面,不再进行梳理

keySet()

public NavigableSet<K> navigableKeySet() {
    KeySet<K> nks = navigableKeySet;
    //如果已经创建了nks,直接返回,否则new一个内部类KeySet
    return (nks != null) ? nks : (navigableKeySet = new KeySet<>(this));
}
//成员变量navigableKeySet
private transient KeySet<K> navigableKeySet;

KeySet

//成员变量m
private final NavigableMap<E, ?> m;
//构造方法,其实就把TreeMap传入了
KeySet(NavigableMap<E,?> map) { m = map; }

//简单看几个方法,发现都是调用m的方法
public int size() { return m.size(); }
public boolean isEmpty() { return m.isEmpty(); }
public boolean contains(Object o) { return m.containsKey(o); }
public void clear() { m.clear(); }

//包括iterator也是通过m访问另一个内部类
public Iterator<E> iterator() {
    if (m instanceof TreeMap)
        return ((TreeMap<E,?>)m).keyIterator();
    else
        return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator();
}

EntrySet、Values的实现也都相差无几,有点类似于外观类,提供受限的方法调用

TreeSet简析

看一下TreeSet的构造函数即可

public TreeSet() {
    this(new TreeMap<E,Object>());
}

TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}

private transient NavigableMap<E,Object> m;

add()

private static final Object PRESENT = new Object();

public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}

显然TreeSet基于TreeMap实现它的基本功能,可以说它就是一个TreeSet,只不过每次add的value是一个final的obj


喜欢 (0)