1. 程式人生 > >jdk TreeMap工作原理分析

jdk TreeMap工作原理分析

fix boolean 不變 變量 get ... delete tails 總結

TreeMap是jdk中基於紅黑樹的一種map實現。HashMap底層是使用鏈表法解決沖突的哈希表,LinkedHashMap繼承自HashMap,內部同樣也是使用鏈表法解決沖突的哈希表,但是額外添加了一個雙向鏈表用於處理元素的插入順序或訪問訪問。

既然TreeMap底層使用的是紅黑樹,首先先來簡單了解一下紅黑樹的定義。

紅黑樹是一棵平衡二叉查找樹,同時還需要滿足以下5個規則:

1.每個節點只能是紅色或者黑點
2.根節點是黑點
3.葉子節點(Nil節點,空節點)是黑色節點
4.如果一個節點是紅色節點,那麽它的兩個子節點必須是黑色節點(一條路徑上不能出現相鄰的兩個紅色節點)
5.從任一節點到其每個葉子節點的所有路徑都包含相同數目的黑色節點

紅黑樹的這些特性決定了它的查詢、插入、刪除操作的時間復雜度均為O(log n)。

一個TreeMap例子

TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>();
treeMap.put(1, "語文");
treeMap.put(2, "數學");
treeMap.put(3, "英語");
treeMap.put(4, "政治");
treeMap.put(5, "物理");
treeMap.put(6, "化學");
treeMap.put(7, "生物");
treeMap.put(
8, "體育");

執行過程:

技術分享圖片

從上面這個例子看到,紅黑樹添加新節點的時候可能會對節點進行旋轉,以保證樹的局部平衡。

TreeMap原理分析

TreeMap內部類Entry表示紅黑樹中的節點:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key; // 關鍵字
    V value; //
    Entry<K,V> left; // 左節點
    Entry<K,V> right; // 右節點
    Entry<K,V> parent; // 父節點
boolean color = BLACK; // 顏色,默認為黑色 Entry(K key, V value, Entry<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } ... }

TreeMap的屬性:

private transient Entry<K,V> root; // 根節點

private transient int size = 0; // 節點個數

put操作

紅黑樹新節點的添加一定是紅色節點,添加完新的節點之後會進行旋轉操作以保持紅黑樹的特性。

為什麽新添加的節點一定是紅色節點,如果添加的是黑色節點,那麽就會導致根到葉子的路徑上有一條路上,多一個額外的黑節點,這個是很難調整的;但是如果插入的是紅色節點,只需要解決其父節點也為紅色節點的這個沖突即可。

以N為新插入節點,P為其父節點,U為其父節點的兄弟節點,R為P和U的父親節點進行分析。如果N的父節點為黑色節點,那直接添加新節點即可,沒有產生沖突。如果出現P節點是紅色節點,那便產生沖突,可以分為以下幾種沖突:

(1) P為紅色節點,且U也為紅色節點,P不論是R的左節點還是右節點

技術分享圖片

將P和U接口變成黑色節點,R節點變成紅色節點。修改之後如果R節點的父節點也是紅色節點,那麽在R節點上執行相同操作,形成了一個遞歸過程。如果R節點是根節點的話,那麽直接把R節點修改成黑色節點。

(2) P為紅色節點,U為黑色節點或缺少,且N是P的右節點、P是R的左節點 或者 P為紅色節點,U為黑色節點或缺少,且N是P的左節點、P是R的右節點

技術分享圖片

這兩種情況分別對P進行左旋和右旋操作。操作結果就變成了沖突3。 (總結起來就是左右變左左,右左變右右)

(3) P為紅色節點,U為黑色節點或缺少,且N是P的左節點、P是R的左節點 或者 P為紅色節點,U為黑色節點或缺少,且N是P的右節點、P是R的右節點

技術分享圖片

這兩種情況分別對祖父R進行右旋和左旋操作。完美解決沖突。(總結起來就是左左祖右,右右祖左)

這3個新增節點的沖突處理方法了解之後,我們回過頭來看本文一開始的例子中添加最後一個[8:體育]節點是如何處理沖突的:

技術分享圖片

接下來我們看TreeMap是如何實現新增節點並處理沖突的。

TreeMap對應的put方法:

public V put(K key, V value) {
    Entry<K,V> t = root;
    if (t == null) { // 如果根節點是空的,說明是第一次插入數據
        compare(key, key);

        root = new Entry<>(key, value, null); // 構造根節點,並賦值給屬性root,默認顏色是黑色
        size = 1; // 節點數 = 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變量裏
            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; // 比較器不存在直接將關鍵字轉換成比較器,如果關鍵字不是一個Comparable接口實現類,將會報錯
        do { // 遍歷尋找節點,關鍵字比節點小找左節點,比節點大的找右節點,直到找到那個葉子節點,會保存需要新構造節點的父節點到parent變量裏
            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); // 構造新的關鍵字節點
    if (cmp < 0) // 需要在左節點構造
        parent.left = e;
    else // 需要在右節點構造
        parent.right = e;
    fixAfterInsertion(e); // 插入節點之後,處理沖突以保持樹符合紅黑樹的特性
    size++;
    modCount++;
    return null;
}

fixAfterInsertion方法處理紅黑樹沖突實現如下:

private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED; // 新增的節點一定是紅色節點

    while (x != null && x != root && x.parent.color == RED) { // P節點是紅色節點並且N節點不是根節點的話一直循環
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // P節點是R節點的左節點
            Entry<K,V> y = rightOf(parentOf(parentOf(x))); // y就是U節點
            if (colorOf(y) == RED) { // 如果U節點是紅色節點,說明P和U這兩個節點都是紅色節點,滿足沖突(1)
                setColor(parentOf(x), BLACK); // 沖突(1)解決方案 把P設置為黑色
                setColor(y, BLACK); // 沖突(1)解決方案 把U設置為黑色
                setColor(parentOf(parentOf(x)), RED); // 沖突(1)解決方案 把R設置為紅色
                x = parentOf(parentOf(x)); // 遞歸處理R節點
            } else { // 如果U節點是黑色節點,滿足沖突(2)或(3)
                if (x == rightOf(parentOf(x))) { // 如果N節點是P節點的右節點,滿足沖突(2)的第一種情況
                    x = parentOf(x);
                    rotateLeft(x); // P節點進行左旋操作
                }
                // P節點左旋操作之後,滿足了沖突(3)的第一種情況或者N一開始就是P節點的左節點,這本來就是沖突(3)的第一種情況
                setColor(parentOf(x), BLACK);  // P節點和R節點交換顏色,P節點變成黑色
                setColor(parentOf(parentOf(x)), RED); // P節點和R節點交換顏色,R節點變成紅色
                rotateRight(parentOf(parentOf(x))); // R節點右旋操作
            }
        } else { // P節點是R節點的右節點
            Entry<K,V> y = leftOf(parentOf(parentOf(x))); // y就是U節點
            if (colorOf(y) == RED) { // 如果U節點是紅色節點,說明P和U這兩個節點都是紅色節點,滿足沖突(1)
                setColor(parentOf(x), BLACK); // 沖突(1)解決方案 把P設置為黑色
                setColor(y, BLACK); // 沖突(1)解決方案 把U設置為黑色
                setColor(parentOf(parentOf(x)), RED); // 沖突(1)解決方案 把R設置為紅色
                x = parentOf(parentOf(x)); // 遞歸處理R節點
            } else { // 如果U節點是黑色節點,滿足沖突(2)或(3)
                if (x == leftOf(parentOf(x))) { // 如果N節點是P節點的左節點,滿足沖突(2)的第二種情況
                    x = parentOf(x);
                    rotateRight(x); // P節點右旋
                }
                // P節點右旋操作之後,滿足了沖突(3)的第二種情況或者N一開始就是P節點的右節點,這本來就是沖突(3)的第二種情況
                setColor(parentOf(x), BLACK); // P節點和R節點交換顏色,P節點變成黑色
                setColor(parentOf(parentOf(x)), RED); // P節點和R節點交換顏色,R節點變成紅色
                rotateLeft(parentOf(parentOf(x))); // R節點左旋操作
            }
        }
    }
    root.color = BLACK; // 根節點是黑色節點
}

fixAfterInsertion方法的代碼跟之前分析的沖突解決方案一模一樣。

get操作

紅黑樹的get操作相比add操作簡單不少,只需要比較關鍵字即可,要查找的關鍵字比節點關鍵字要小的話找左節點,否則找右節點,一直遞歸操作,直到找到或找不到。代碼如下:

final Entry<K,V> getEntry(Object key) {
    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;
}

remove操作

紅黑樹的刪除節點跟添加節點一樣,比較復雜,刪除節點也會讓樹不符合紅黑樹的特性,也需要解決這些沖突。

刪除操作分為2個步驟:

  1. 將紅黑樹當作一顆二叉查找樹,將節點刪除
  2. 通過”旋轉和重新著色”等一系列來修正該樹,使之重新成為一棵紅黑樹

步驟1的刪除操作可分為幾種情況:

  1. 刪除節點沒有兒子:直接刪除該節點
  2. 刪除節點有1個兒子:刪除該節點,並用該節點的兒子節點頂替它的位置
  3. 刪除節點有2個兒子:可以轉成成刪除節點只有1個兒子的情況,跟二叉查找樹一樣,找出節點的右子樹的最小元素(或者左子樹的最大元素,這種節點稱為後繼節點),並把它的值轉移到刪除節點,然後刪除這個後繼節點。這個後繼節點最多只有1個子節點(如果有2個子節點,說明還能找出右子樹更小的值),所以這樣刪除2個兒子的節點就演變成了刪除沒有兒子的節點和刪除只有1個兒子的節點的情況

刪除節點之後要考慮的問題就是紅黑樹失衡的調整問題。

步驟2遇到的調整問題只有2種情況:

  1. 刪除節點沒有兒子節點
  2. 刪除節點只有1個兒子節點

刪除節點沒有兒子節點的話,直接把節點刪除即可。如果節點是黑色節點,需要進行平衡性調整,否則,不用調整平衡性。這裏的平衡性調整跟刪除只有1個兒子節點一樣,刪除只有1個兒子的調整會先把節點刪除,然後兒子節點頂上來,頂上來之後再進行平衡性調整。而刪除沒有兒子節點的節點的話,先進行調整,調整之後再把這個節點刪除。他們的調整策略是一樣的,只不過沒有兒子節點的情況下先進行調整,然後再刪除節點,而有兒子節點的情況下,先把節點刪除,刪除之後兒子節點頂上來,然後再做平衡性調整。

刪除節點只有1個兒子節點還分幾種情況:

  1. 如果被刪除的節點是紅色節點,那說明它的父節點是黑色節點,兒子節點也是黑色節點,那麽刪除這個節點就不會影響紅黑樹的屬性,直接使用它的黑色子節點代替它即可
  2. 如果被刪除的節點是黑色節點,而它的兒子節點是紅色節點。刪除這個黑色節點之後,它的紅色兒子節點頂替之後,會破壞性質5,只需要把兒子節點重繪為黑色節點即可,這樣原先通過黑色刪除節點的所有路徑被現在的重繪後的兒子節點所代替
  3. 如果被刪除的節點是黑色節點,而它的兒子節點也是黑色節點。這是一種復雜的情況,因為路徑路過被刪除節點的黑色節點路徑少了1個,導致違反了性質5,所以需要對紅黑樹進行平衡調整。可分為以下幾種情況進行調整:

以N為刪除節點的兒子節點(刪除之後,處於新的位置上),它的兄弟節點為S,它們的父節點為P,Sl和Sr為S節點的左右子節點為例,進行講解,其中N是父節點P的左子節點,如果N是父節點P的右子節點,做對稱處理。

3.1:N是新的根節點。這種情況下不用做任何處理,因為原先的節點也是一個根節點,相當於所有的路徑都需要經過這個根節點,刪除之後沒有什麽影響,而且新根也是黑色節點,符合所有特性,不需要進行調整

3.2: S節點是紅色節點,那麽P節點,Sl,Sr節點是黑色節點。在這種情況下,對P節點進行左選操作並且交換P和S的顏色。完成這2個操作之後,所有路徑上的黑色節點沒有變化,但是N節點有了一個黑色兄弟節點Sl和一個紅色的父親節點P,左子樹刪除節點後還有存在著少1個黑色節點路徑的問題。接下來按照N節點新的位置(兄弟節點S是個黑色節點,父節點P是個紅色節點)進行3.4、3.5或3.6情況處理

技術分享圖片

3.3:N的父親節點P、兄弟節點S,還有S的兩個子節點Sl,Sr均為黑色節點。在這種情況下,重繪S為紅色。重繪之後路過S節點這邊的路徑跟N節點一樣也少了一個黑色節點,但是出現了另外一個問題:不經過P節點的路徑還是少了一個黑色節點。 接下來,要調整以P作為N遞歸調整樹

技術分享圖片

3.4:S和S的兒子節點Sl、Sr為黑色節點,但是N的父親節點P為紅色節點。在這種情況下,交換N的兄弟S與父親P的顏色,顏色交換之後左子樹多了1個黑色節點路徑,剛好填補了左子樹刪除節點的少一個黑色節點路徑的問題,而右子樹的黑色路徑沒有改變,解決平衡問題

技術分享圖片

3.5:S是黑色節點,S的左兒子節點Sl是紅色,S的右兒子節點Sr是黑色節點。在這種情況下,在S上做右旋操作交換S和它新父親的顏色。操作之後,左子樹的黑色節點路徑和右子樹的黑色節點路徑沒有改變。但是現在N節點有了一個黑色的兄弟節點,黑色的兄弟節點有個紅色的右兒子節點,滿足了3.6的情況,按照3.6處理

技術分享圖片

3.6:S是黑色節點,S的右兒子節點Sr為紅色節點,S的左兒子Sl是黑色節點,P是紅色或黑色節點。在這種情況下,N的父親P做左旋操作,交換N父親P和S的顏色,S的右子節點Sr變成黑色。這樣操作以後,左子樹的黑色路徑+1,補了刪除節點的黑色路徑,右子樹黑色路徑不變,解決平衡問題

技術分享圖片

了解了刪除節點之後的平衡性調整之後,我們回過頭來看本文一開始的例子進行節點刪除的操作過程:

技術分享圖片

TreeMap刪除方法如下:

private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--; // 節點個數 -1

    if (p.left != null && p.right != null) { // 如果要刪除的節點有2個子節點,去找後繼節點
        Entry<K,V> s = successor(p); // 找出後繼節點
        p.key = s.key; // 後繼節點的關鍵字賦值給刪除節點
        p.value = s.value; // 後繼節點的值賦值給刪除節點
        p = s; // 改為刪除後繼節點
    }

    Entry<K,V> replacement = (p.left != null ? p.left : p.right); // 找出替代節點,左子樹存在的話使用左子樹,否則使用右子樹。這個替代節點就是被刪除節點的左子節點或右子節點

    if (replacement != null) { // 替代節點如果存在的話
        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;

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

// 刪除節點後的平衡性調整,對應之前分析的節點昵稱,N、S、P、Sl、Sr
private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) { // N節點是黑色節點並且不是根節點就一直循環
        if (x == leftOf(parentOf(x))) { // 如果N是P的左子節點
            Entry<K,V> sib = rightOf(parentOf(x)); // sib就是N節點的兄弟節點S

            if (colorOf(sib) == RED) { // 如果S節點是紅色節點,滿足刪除沖突3.2,對P節點進行左旋操作並交換P和S的顏色
                // 交換P和S的顏色,S原先為紅色,P原先為黑色(2個紅色節點不能相連)
                setColor(sib, BLACK); // S節點從紅色變成黑色
                setColor(parentOf(x), RED); // P節點從黑色變成紅色
                rotateLeft(parentOf(x)); // 刪除沖突3.2中P節點進行左旋
                sib = rightOf(parentOf(x)); // 左旋之後N節點有了一個黑色的兄弟節點和紅色的父親節點,S節點重新賦值成N節點現在的兄弟節點。接下來按照刪除沖突3.4、3.5、3.6處理
            }

            // 執行到這裏S節點一定是黑色節點,如果是紅色節點,會按照沖突3.2交換成黑色節點
            // 如果S節點的左右子節點Sl、Sr均為黑色節點並且S節點也為黑色節點
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                // 按照刪除沖突3.3和3.4進行處理
                // 如果是沖突3.3,說明P節點也是黑色節點
                // 如果是沖突3.4,說明P節點是紅色節點,P節點和S節點需要交換顏色
                // 3.3和3.4沖突的處理結果S節點都為紅色節點,但是3.4沖突處理完畢之後直接結束,而3.3沖突處理完畢之後繼續調整
                setColor(sib, RED); // S節點變成紅色節點,如果是3.4沖突需要交換顏色,N節點的顏色交換在跳出循環進行
                x = parentOf(x); // N節點重新賦值成N節點的父節點P之後繼續遞歸處理
            } else { // S節點的2個子節點Sl,Sr中存在紅色節點
                if (colorOf(rightOf(sib)) == BLACK) { // 如果S節點的右子節點Sr為黑色節點,Sl為紅色節點[Sl如果為黑色節點的話就在上一個if邏輯裏處理了],滿足刪除沖突3.5
                    // 刪除沖突3.5,對S節點做右旋操作,交換S和Sl的顏色,S變成紅色節點,Sl變成黑色節點
                    setColor(leftOf(sib), BLACK); // Sl節點變成黑色節點
                    setColor(sib, RED); // S節點變成紅色節點
                    rotateRight(sib); // S節點進行右旋操作
                    sib = rightOf(parentOf(x)); // S節點賦值現在N節點的兄弟節點
                }
                // 刪除沖突3.5處理之後變成了刪除沖突3.6或者一開始就是刪除沖突3.6
                // 刪除沖突3.6,P節點做左旋操作,P節點和S接口交換顏色,Sr節點變成黑色
                setColor(sib, colorOf(parentOf(x))); // S節點顏色變成P節點顏色,紅色
                setColor(parentOf(x), BLACK); // P節點變成S節點顏色,也就是黑色
                setColor(rightOf(sib), BLACK); // Sr節點變成黑色
                rotateLeft(parentOf(x)); // P節點做左旋操作
                x = root; // 準備跳出循環
            }
        } else { // 如果N是P的右子節點,處理過程跟N是P的左子節點一樣,左右對換即可
            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); // 刪除沖突3.4循環調出來之後N節點顏色設置為黑色 或者 刪除節點只有1個紅色子節點的時候,將頂上來的紅色節點設置為黑色
}

參考資料

http://dongxicheng.org/structure/red-black-tree/

http://blog.csdn.net/chenssy/article/details/26668941

http://zh.wikipedia.org/wiki/%E7%BA%A2%E9%BB%91%E6%A0%91

http://www.cnblogs.com/fanzhidongyzby/p/3187912.html

jdk TreeMap工作原理分析