【數據結構】11.java源碼關於TreeMap

目錄


1.TreehMap的內部結構
2.TreehMap構造函數
3.元素新增策略
4.元素刪除
5.元素修改和查找
6.特殊操作
7.擴容
8.總結

 

1.TreeMap的內部結構

 

 

 

 

首先確認一點,treemap是一個基於紅黑樹的map,這個集合的一個特點就是排序,是的如果不是排序,那麼hashmap可以完美取代

再開始前我們要熟悉一個紅黑樹的概念:

對於紅黑樹的定義:

1.節點是紅色或黑色。
2.根是黑色。
3.所有葉子都是黑色(葉子是NIL節點)。
4.每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)
5.從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。

 

2.TreeMap構造函數

通過已排序的map進行新的treemap的構造生成,這裏用到了一個buildFromSorted函數,這個方法會遞歸數據

參考buildFromSorted實現,這個實現的紅黑樹,說白了,就是把最後一層變成紅色,以上全作爲黑色

 

3.元素新增策略

Put操作

這個操作沒啥,就是遍歷這顆樹,左邊小,右邊大,遍歷到合適的位置設置值,或者創建新的節點插入,並默認設置爲黑色
重點在於後面的變動之後,如果進行紅黑樹的修復
針對紅黑樹的變動,可以參考以上總結的規則:https://www.cnblogs.com/cutter-point/p/10976416.html

針對於紅黑樹的操作主要就是左旋和右旋的操作

public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            //如果根節點爲空,然後這個比較其實是起一個類型檢查作用
            compare(key, key);
            //創建root節點
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        //如果根節點存在
        int cmp;
        Entry<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();
            //就提前key的比較器
            @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);
        }
        //如果一直到t爲空還沒找到,那麼就創建新值
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        //進行紅黑樹修復
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }


    //獲取對應節點的父節點
    private static <K,V> Entry<K,V> parentOf(Entry<K,V> p) {
        return (p == null ? null: p.parent);
    }

    //求當前節點的左右節點
    private static <K,V> Entry<K,V> leftOf(Entry<K,V> p) {
        return (p == null) ? null: p.left;
    }

    private static <K,V> Entry<K,V> rightOf(Entry<K,V> p) {
        return (p == null) ? null: p.right;
    }

    //獲取當前節點的顏色,爲空即爲黑
    private static <K,V> boolean colorOf(Entry<K,V> p) {
        return (p == null ? BLACK : p.color);
    }

    private static <K,V> void setColor(Entry<K,V> p, boolean c) {
        if (p != null)
            p.color = c;
    }


紅黑樹的修復操作,主要重點就是對從一個節點到葉子節點的黑色節點個數相同爲基準

//進行紅黑樹修復
    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)));
                //判斷另外一個節點是紅是黑
                if (colorOf(y) == RED) {
                    //如果祖父的兄弟節點是紅色,那麼主要是吧兄弟節點改成黑色即可,這樣祖父的兄弟節點相當於增加了一個黑色個數
                    //如果是紅,那麼就直接修改顏色即可
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //如果是黑色,說明兩邊的分支走到葉子節點的不是相同數目的黑色節點
                    //如果x是右節點,那麼就左旋,如果是左節點就右旋
                    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;
    }


左旋右旋操作就是把以對應的節點爲核心進行節點的上升和下降,然後要複合紅黑樹的規範
private void rotateLeft(Entry<K,V> p) {
        //左旋操作
        if (p != null) {
            //如果節點不爲空,進行左旋的時候,獲取節點的右節點
            Entry<K,V> r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            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;
        }
    }

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

4.元素刪除

進行元素刪除的思想是:
如果我需要刪除這個節點,那麼首選判斷右子樹是否存在,如果存在那麼就找到右邊子樹的最小值,也就是最小的葉子節點,用一個最靠近的節點的數據替換需要刪除的節點的數據,這樣就保障二叉索引樹的特性,然後根據變動的節點顏色重新修復這顆紅黑樹的顏色

 

    //節點刪除操作
    public V remove(Object key) {
        //獲取到這個節點
        Entry<K,V> p = getEntry(key);
        if (p == null)
            return null;

        //獲取舊值
        V oldValue = p.value;
        //刪除節點,然後返回舊值
        deleteEntry(p);
        return oldValue;
    }



final Entry<K,V> getEntry(Object key) {
        //如果有設置比較器,那麼就優先使用比較器進行尋找
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        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;
    }


//進行節點的刪除
    private void deleteEntry(Entry<K,V> p) {
        modCount++;
        size--;

        // 當前節點有兩個子節點,找到右子樹中最小元素作爲後繼節點;將後繼節點信息替換到當前節點
        if (p.left != null && p.right != null) {
            //說白了就是找這個p元素相鄰大小的元素,優先找大的,其次找小的
            //1.找右子樹的最小節點  因爲上面有判斷p的左右節點必須存在,所以結果肯定是右子樹的最小值
            Entry<K,V> s = successor2(p);
            p.key = s.key;
            p.value = s.value;
            //吧引用指向s,吧p的值設置爲s的值
            //這個時候需要刪除的那個節點的值變成了一個新的最靠近的值,這樣就不會破壞索引樹的條件,然後把那個用來替換的節點幹掉即可
            p = s;
            //替換刪除的元素
        }

        // 開始修復,優先取這個節點的左邊,否則取右邊作爲replacement節點對象
        //除非是p.right和left有一個爲空,不然一般肯定走的是p.right並且是個null對象
        Entry<K,V> replacement = (p.left != null ? p.left : p.right);

        //如果要進行取代的節點爲空,那麼就不用操作
//        1、有兩個兒子。這種情況比較複雜,但還是比較簡單。上面提到過用子節點C替代代替待刪除節點D,然後刪除子節點C即可。
//        2、沒有兒子,即爲葉結點。直接把父結點的對應兒子指針設爲NULL,刪除兒子結點就OK了。
//        3、只有一個兒子。那麼把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。
        if (replacement != null) {
            //把需要替換的節點的父節點設置爲p的父節點
            replacement.parent = p.parent;
            //判斷p的父節點是否Wie空,或者判斷p是否是作爲左節點,否則判斷是否是右節點
            //說白了就是用replacement 取代P節點
            if (p.parent == null)
                root = replacement;
            else if (p == p.parent.left)
                p.parent.left  = replacement;
            else
                p.parent.right = replacement;

            //設置p的左右和父節點爲空,也就是吧p節點剝離開
            p.left = p.right = p.parent = null;

            // 修復顏色分配,因爲最後一個要刪除的節點是黑色,那麼刪除這個節點之後,這條線的黑色節點個數肯定會減去1
            if (p.color == BLACK)
                //那麼我們就需要對這個變動過的節點進行調整
                fixAfterDeletion(replacement);
        } else if (p.parent == null) { // return if we are the only node.
            root = null;
        } else {
            //一般情況是這個,也就是說吧需要刪除的節點移動到末尾葉子節點,然後把key,value替換掉,最後刪除調最後一個葉子即可
            //然後如果葉子的顏色正好是黑色的,那麼要重新修復顏色
            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;
            }
        }
    }



static <K,V> TestTreeMap.Entry<K,V> successor2(Entry<K,V> t) {
        //找到右邊節點的最小元素,也就是僅僅比T大一點的元素
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    }


private void fixAfterDeletion(Entry<K,V> x) {
        //判斷替換過的節點是否是黑色,刪除節點需要一直迭代,直到 x 不是根節點,且 x 的顏色是黑色
        while (x != root && colorOf(x) == BLACK) {
            //如果這個節點是左節點
            if (x == leftOf(parentOf(x))) {
                //獲取兄弟節點
                Entry<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
                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);
    }

5.元素修改和查找

 

修改可以參考put操作,對key無影響

查找因爲是二叉索引樹,所以查找方式和remove中的getEntry操作類似


6.特殊操作

6.1BuildFromSorted

這函數用來根據一級拍好順序的map構建treemap

 

 

再這個函數之前還有一個computeRedLevel函數,這個用來計算當前節點所在的層數

 

 //計算紅色節點應該在紅黑樹哪一層,因爲二叉樹,因爲每層二叉樹要填滿的話必須是2的倍數
    //每層數據疊加是1,1+2,1+2+4,1+2+4+8.。。 基本就是每層就是每層/2
    //sz指樹中節點的個數,爲了確保是一個紅黑樹,那麼需要把前面幾層全部看成黑色,最後一層設置爲紅色即可
    //因爲sz是節點的個數,所以最後一個節點所在的層數即是紅色
    public static int computeRedLevel(int sz) {
        int level = 0;
        //從0開始計算0,2,6,14
        //可以看出m=(m+1)*2 前一個和後一個的遞推關係 每一層計算
        //那麼反過來就是m/2-1就是上一層的位置,最後一個m>=0的時候還要計算一次
        for (int m = sz - 1; m >= 0; m = m / 2 - 1)
            level++;
        return level;
    }


    //通過迭代構造一個新的排序的map,遞歸將SortedMap中的元素逐個關聯
    //str: 如果不爲空,則從流裏讀取key-value,defaultVal:見名知意,不爲空,則value都用這個值
    private void buildFromSorted(int size, Iterator<?> it,
                                 java.io.ObjectInputStream str,
                                 V defaultVal)
            throws  java.io.IOException, ClassNotFoundException {
        this.size = size;
        //遞歸,添加到root節點上
        root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
                it, str, defaultVal);
    }


   /**
     * level: 當前樹的層數,注意:是從0層開始
     * lo: 子樹第一個元素的索引
     * hi: 子樹最後一個元素的索引
     * redLevel: 上述紅節點所在層數
     * 剩下的3個就不解釋了,跟上面的一樣
     */
    private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
                                             int redLevel,
                                             Iterator<?> it,
                                             java.io.ObjectInputStream str,
                                             V defaultVal)
            throws  java.io.IOException, ClassNotFoundException {
        /*
         * Strategy: The root is the middlemost element. To get to it, we
         * have to first recursively construct the entire left subtree,
         * so as to grab all of its elements. We can then proceed with right
         * subtree.
         *
         * The lo and hi arguments are the minimum and maximum
         * indices to pull out of the iterator or stream for current subtree.
         * They are not actually indexed, we just proceed sequentially,
         * ensuring that items are extracted in corresponding order.
         */
        if (hi < lo) return null;

        //這相當於除以二,取中間位置,相當於除以2
        int mid = (lo + hi) >>> 1;

        Entry<K,V> left  = null;
        //子樹第一個元素的索引開始到中間的位置作爲左子樹,右邊剩下遞歸又右子樹
        if (lo < mid) //遞歸左邊部分節點
            left = buildFromSorted(level+1, lo, mid - 1, redLevel,
                    it, str, defaultVal);

        K key;
        V value;
        //通過迭代器遍歷所有節點
        if (it != null) {
            if (defaultVal==null) {
                Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
                key = (K)entry.getKey();
                value = (V)entry.getValue();
            } else {
                key = (K)it.next();
                value = defaultVal;
            }
        } else { // use stream,通過流讀取對象
            key = (K) str.readObject();
            value = (defaultVal != null ? defaultVal : (V) str.readObject());
        }

        //創建中間節點
        Entry<K,V> middle =  new Entry<>(key, value, null);

        // color nodes in non-full bottommost level red
        if (level == redLevel)
            middle.color = RED;

        if (left != null) {
            middle.left = left;
            left.parent = middle;
        }

        //遞歸右邊節點
        if (mid < hi) {
            Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
                    it, str, defaultVal);
            middle.right = right;
            right.parent = middle;
        }

        return middle;
    }

這裏可以總結一點:左旋和右旋的判斷主要依據是=》從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。因爲我們每次新增的節點都是紅色,所以這個紅色的節點就會破壞原來的結構,會再紅色的null節點新增一個黑色null節點,爲了修復這種情況,那麼就需要對父節點下的另外一個節點進行修復

 

7.擴容

不存在擴容問題,二叉樹嘛,更類似鏈表的結構

 

8.總結

總結就是不論是新增還是刪除,再修復顏色的時候,維持從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點這個原則
所以一般會校驗這個節點的兄弟,以及父輩的兄弟節點,至於進行右旋還是左旋,這個參考我之前的博客的內容以及規則

 


參考:

https://blog.csdn.net/cyywxy/article/details/81151104
https://www.jianshu.com/p/e11fe1760a3d

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章