JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结

了解 Tree 之前我们必须了解 红黑树 因为Tree 的数据结构就是红黑树

  • 红黑树的特性

    • (1)每个节点或者是黑色,或者是红色。
    • (2)根节点是黑色。
    • (3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
    • (4)如果一个节点是红色的,则它的子节点必须是黑色的。
    • (5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
  • 注意:

    • (01) 特性(3)中的叶子节点,是只为空(NIL或null)的节点。
    • (02) 特性(5),确保没有一条路径会比其他路径长出俩倍。因而,红黑树是相对是接近平衡的二叉树。

当我们 进行插入 删除的 我们必定会改变红黑树的 规则 而,我们的措施就是调整策略就是

  • 1.改变某个节点的颜色使之符合规则
  • 2.改变树的结构关系 进行左旋或者有旋

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 红黑树
《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 mark

红黑树的基本操作(一) 左旋和右旋

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 左旋

java 版本左旋代码(TreeMap 中的)

左旋的过程是将 E 的右子树绕E逆时针旋转,使得 E 的右子树成为x的父亲,同时修改相关节点的引用。旋转之后,二叉查找树的属性仍然满足。

                                //传入 动图中的 E
        private void rotateLeft(Entry<K,V> p) {
        
        if (p != null) {
            //E的右子树
            Entry<K,V> r = p.right;
            // E的 右子树改变   改变成为S 的左子树
            p.right = r.left;
            //判断S 的左子树是否空 不为空改变 S 的左子树的parent 为E
            if (r.left != null)
                r.left.parent = p;
                
            //使S成为主节点
            r.parent = p.parent;
            
            //判断 是否原先的父节点为空 为空的话 S为父节点
            if (p.parent == null)
                root = r;
                
            // 判断 E到底是 父节点的左节点还是右节点  将父节点的左(右)节点编程S节点
            else if (p.parent.left == p)
                p.parent.left = r;
                
                
            else
                p.parent.right = r;
            //将S的左节点 设为 E
            r.left = p;
            //E的parent 节点 设为S
            p.parent = r;
        }
    }

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 右旋

基本和左旋代码保持一致

    /** From CLR */
    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;
        }
    }

红黑树的基本操作 查(Tree get())

插入回导致红黑树的性质发生改变 有以下几种情况

    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
    //  comparator 这个是 个成员变量 外部设置特定的 比较器 有就用这个  这个变量 可以初始化的时候 放进去
    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;
    // 利用比较器的特性开始比较大小  相同 return 小于 从左子树开始 大了 从右子树开始 
    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;
}

红黑树的基本操作(二) 添加 put

  • 第一步: 将红黑树当作一颗二叉查找树,将节点插入。

     红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。
     也就意味着,树的键值仍然是有序的。
     此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。
     这也就意味着,任何的旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
     
     好吧?那接下来,我们就来想方设法的旋转以及重新着色,使这颗树重新成为红黑树!
    
  • 第二步:将插入的节点着色为”红色”。

      将插入的节点着色为红色,不会违背"特性(5)"
    
选项现象说明问题分析处理策略
1空树中插入根节点初始插入的节点均为红色,因此简单将红色重涂为黑色即可。
2插入节点的父节点是黑色插入的红色节点,未违反任何性质 无需调整。
3当前节点的父节点是红色,且叔叔节点(祖父节点的另一个子节点)也是红色。此时祖父节点一定存在,否则插入前就已不是红黑树。与此同时,又分为父节点是祖父节点的左子还是右子,由于对称性,我们只要解开一个方向就可以了。在此,我们只考虑父节点为祖父左子的情况。同时,还可以分为当前结点是其父结点的左子还是右子,但是处理方式是一样的。我们将此归为同一类。 (01) 将“父节点”设为黑色. (02) 将“叔叔节点”设为黑色。 (03) 将“祖父节点”设为“红色”。 (04) 将“祖父节点”设为“当前节点”(红色节点); 之后继续对“当前节点”进行操作。
4当前节点的父节点是红色,叔叔节点是黑色,当前节点是右子节点。 (01) 将“父节点”作为“新的当前节点”。 (02) 以“新的当前节点”为支点进行左旋。
5当前节点的父节点是红色,叔叔节点是黑色,当前节点是左子节点。 (01) 将“父节点”设为“黑色”。 (02) 将“祖父节点”设为“红色”。 (03) 以“祖父节点”为支点进行右旋。

情况三:

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 情况三

有情况三的图我们可以看到当前节点7的父节点也为红色,出现父子节点都为红色的情况,且叔叔节点为黑色,

因此适用于情况4:当前节点的父节点是红色,叔叔节点是黑色,当前节点是右子节点,

那么按照情况4的恢复策略,进行新一轮的旋转或涂色,如下看情况4如何进行调整。

情况四:

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 情况四

这里作的操作为:当前节点由原来的7变换为其父节点2,以新的当前节点2,作左旋操作.

如上图。操作完成后,发现父子节点仍都是红色,继续进行旋转或涂色。

这里适用于情况5:当前节点的父节点是红色,叔叔节点是黑色,当前节点是左子节点来进行再次调整,请看下面的情况5如何进行调整。

  下面谈谈为什么要这样处理。(建议理解的时候,通过下面的图进行对比)
  首先,将“父节点”作为“新的当前节点”;接着,以“新的当前节点”为支点进行左旋。 为了便于理解,我们先说明第(02)步,再说明第(01)步;为了便于说明,我们设置“父节点”的代号为F(Father),“当前节点”的代号为S(Son)。
为什么要“以F为支点进行左旋”呢?根据已知条件可知:S是F的右孩子。而之前我们说过,我们处理红黑树的核心思想:
将红色的节点移到根节点;然后,将根节点设为黑色。  
既然是“将红色的节点移到根节点”,那就是说要不断的将破坏红黑树特性的红色节点上移(即向根方向移动)。   
而S又是一个右孩子,因此,我们可以通过“左旋”来将S上移!   
按照上面的步骤(以F为支点进行左旋)处理之后:若S变成了根节点,那么直接将其设为“黑色”,就完全解决问题了;  
若S不是根节点,那我们需要执行步骤(01),即“将F设为‘新的当前节点’”。  
那为什么不继续以S为新的当前节点继续处理,而需要以F为新的当前节点来进行处理呢?  
这是因为“左旋”之后,F变成了S的“子节点”,即S变成了F的父节点;  
而我们处理问题的时候,需要从下至上(由叶到根)方向进行处理;  
也就是说,必须先解决“孩子”的问题,再解决“父亲”的问题;  
所以,我们执行步骤(01):将“父节点”作为“新的当前节点”。

情况五:

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 情况五

下面谈谈为什么要这样处理。(建议理解的时候,通过下面的图进行对比)

为了便于说明,我们设置“当前节点”为S(OriginalSon),“兄弟节点”为B(Brother),  
“叔叔节点”为U(Uncle),“父节点”为F(Father),祖父节点为G(Grand-Father)。  
      
S和F都是红色,违背了红黑树的“特性(4)”,我们可以将F由“红色”变为“黑色”,就解决了“违背‘特性(4)’”的问题;   
但却引起了其它问题:违背特性(5),因为将F由红色改为黑色之后,所有经过F的分支的黑色节点的个数增加了1。  
那我们如何解决“所有经过F的分支的黑色节点的个数增加了1”的问题呢? 我们可以通过“将G由黑色变成红色”,同时“以G为支点进行右旋”来解决。
此时,树已经满足红黑树的性质,如果仍不满足,则仍按照情况1——情况5的方式进行旋转和重新涂色。

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 put

put代码

       public V put(K key, V value) {
       
        Entry<K,V> t = root;
        //根节点为空
        if (t == null) {
            
            compare(key, key); // type (and possibly null) check

            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; //设置的比较器
        // 如果比较器不为空 按照 设置的比较器进行比较  如果为空 按照 key 实现的比较器方法进行比较
        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  //相同的话替换掉 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);
        }
        
        // 没有在树上找到 new 出一个entry   根据最后比较结果 设置这个是在左树 还是右树
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
            
        // fix 就是 修正二叉树     
        fixAfterInsertion(e);
        size++;// 增加 size 
        modCount++;  // 返回修改次数
        return null;
    }

修正二叉树结构

    /** From CLR */
    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)))) {
            
            
                // X 的 叔叔节点
                Entry<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 {
            
            
                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; 
    }


寻找节点后继(树中比大于 当前节点 的最小的那个元素)

t的右子树不空,则t的后继是其右子树中最小的那个元素。
t的右孩子为空,则t的后继是其第一个向左走的祖先。

为什么寻找中序后继节点? 后继节点在红黑树的删除操作中将会用到。

    static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    
        if (t == null)
            return null;
        // 如果right 不为空 往左
        else if (t.right != null) {
        
            Entry<K,V> p = t.right;
            // while 循环找到中序后继结点  一直往左找
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            
            Entry<K,V> p = t.parent;
            
            Entry<K,V> ch = t;
            
                        // while 循环找到中序后继结点  一直往右找

            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

Remove

  • 第一步:将红黑树当作一颗二叉查找树,将节点删除。
    这和”删除常规二叉查找树中删除节点的方法是一样的”。分3种情况:

    • ① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
    • ② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
    • ③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给”被删除节点”之后,再将后继节点删除。这样就巧妙的将问题转换为”删除后继节点”的情况了,下面就考虑后继节点。 在”被删除节点”有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然”的后继节点”不可能双子都非空,就意味着”该节点的后继节点”要么没有儿子,要么只有一个儿子。若没有儿子,则按”情况① “进行处理;若只有一个儿子,则按”情况② “进行处理。
  • 第二步:通过”旋转和重新着色”等一系列来修正该树,使之重新成为一棵红黑树。
    因为”第一步”中删除节点之后,可能会违背红黑树的特性。所以需要通过”旋转和重新着色”来修正该树,使之重新成为一棵红黑树。

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 重新着色 现象


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

        V oldValue = p.value;
        deleteEntry(p);
        
        //弹出旧值
        return oldValue;
    }


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

    //  删除点p的左右子树都非空
        if (p.left != null && p.right != null) {
            Entry<K,V> s = successor(p);  //找出 中序后继 节点
            p.key = s.key;
            p.value = s.value;
            p = s;
        } // p has 2 children

        // Start fixup at replacement node, if it exists.
        Entry<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;

            // Null out links so they are OK to use by fixAfterDeletion.
            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;
            }
        }
    }

// todo 关于删除 跟多解释可以查看参考文章

TreeSet

TreeSet是对TreeMap的简单包装,对TreeSet的函数调用都会转换成合适的TreeMap方法,因此TreeSet的实现非常简单。和实现LinkedSet 的方式 一样这里不再赘述。
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{

    private transient NavigableMap<E,Object> m;

    public TreeSet() {
    this(new TreeMap<E,Object>());
    }
    TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
    }
    
    
    public int size() {
        return m.size();
    }
    public boolean isEmpty() {
        return m.isEmpty();
    }
    public boolean remove(Object o) {
        return m.remove(o)==PRESENT;
    }
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    .........   
    .........

}

总结

看了TreeMap 其实我首先是不太愿意分析整的 ,为什么呢? 因为我数据结构学的不太好
特别是 红黑树 在我理解里面 属于高级数据结构了 这篇文章 距离 我上篇文章就开始写了 写了三四天 也看了很多 文章 自己也对红黑树加深了了解 O(logn) 这也算是给HashMap 给了个尾巴
因为HashMap 到八个就会链表转 红黑树 那时候就想分析来着,刚还TreeMap 就是红黑树 一起分析了 ,TreeMap从源码因为注释
也看出了 源码也是参考过其他代码的 其实 并不难 我们只要一句句分析 结合红黑树特点 我们也能看懂

(看代码的时候我就在想 我什么时候能写出这种 代码 卧槽还有这种操作 之类的 )

到此 java 集合框架 1.8 的源码 我们基本上看了一遍

基本上 主要由 这几种 数据结构 链表 数组 Hash表 红黑树

这些数据结构组合成为我们高可用的容器框架

关于 下一阶段的 写作计划 我可能 会写一个系列的 并发 (同时自己也深入了解) spring 框架也可能会写一系列 还有 日常bug 总结

参考资料:

我的公众号

《JDK1.8 之 集合框架 TreeMap TreeSet 源码解析 容器框架总结》 微信公众号

    原文作者:Gxgeek
    原文地址: https://www.jianshu.com/p/69f11fc9ea38
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞