研究jdk關於TreeMap 紅黑樹算法實現


因為TreeMap的實現方式是用紅黑樹這種數據結構進行存儲的,所以呢我主要通過分析紅黑樹的實現在看待TreeMap,側重點也在於如何實現紅黑樹,因為網上已經有非常都的關於紅黑樹的實現。我也看了些,但是有的說的不是很清楚,有的解釋的也很清晰。這邊主要是我的思路的總結。因為之前在研究HashMap和CurrentHashMap源碼的時候有涉及到,文章是探索HashMap實現原理及其在jdk8數據結構的改進和另一篇探索jdk8之ConcurrentHashMap 的實現機制,但是關於插入和刪除分析的還不是很仔細。所以有了這篇文章。

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

預報知識:在講解紅黑樹之前需要對數的數據結構有所了解,對二叉樹結構以及實現由所了解,對二叉查找樹ADT結構以及實現有所理解。如果對這些基礎知識不是很清楚的話,一開始就看java的jdk關於紅黑樹的實現就會很吃力。如果對這些知識已經遺忘的話建議大學教材關於樹的那張在看看。或者參看《數據結構與算法-java語言描述》這邊是關於樹和高級數據結構及實現這兩章來理解。

本文只重點分析下插入和刪除這兩種比較復雜的情況。
關於插入
自定向上插入
插入分為幾種情況:1、節點為黑色就直接插入;2、如果節點為紅色那么插入紅色就回違反性質4,如果插入黑色就回違反性質5。
那么如果是情況2要怎么處理呢?首先插入紅色是肯定的,因為如果你插入黑色就會導致不平衡。那么插入紅色后違反了性質4怎么辦?就要旋轉,如AVL樹一樣。

上圖只是一般情況。如果X節點父類節點P的兄弟節點S是紅色的,那么我們旋轉后就回變成G-S是兩個相連的紅色節點,違反了性質4。那么怎么處理呢?那就涉及到自頂向下的紅黑樹。

上濾過程修復紅黑樹
修復紅黑樹,其實是一種上濾的過程,因為你插入的時候每次都循環修復這個過程,后面插入的就是一個向上的過程。接上面,如果出現了G-S紅紅的節點怎么處理?那么如果是這種情況的話我們是這樣處理的,因為P的兄弟節點S也是紅色的,我們會先執行一次顏色翻轉。翻轉后P和S就變成了黑色而G變成了紅色,然后在旋轉。

下面關於insert的操作,代碼來着java的TreeMap,我覺得這個代碼的實現方式是最簡答明了的

public V put(K key, V value) 
 { 
    Entry<K,V> t = root; 
    // 一個空鏈表
    if (t == null) 
    { 
        root = new Entry<K,V>(key, value, null); 
        size = 1; 
        // 記錄修改次數為 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(); 
        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); 
    } 
    // 將新插入的節點作為 parent 節點的子節點
    Entry<K,V> e = new Entry<K,V>(key, value, parent); 
    if (cmp < 0) 
        parent.left = e; 
    else 
        parent.right = e; 
    // 修復紅黑樹
    fixAfterInsertion(e);                               
    size++; 
    modCount++; 
    return null; 
 }

TreeMap 為插入節點后的修復操作由 fixAfterInsertion(Entry<K,V> x) 方法提供,該方法的源代碼如下

private void fixAfterInsertion(Entry<K,V> x) 
 { 
    x.color = RED; 
    // 直到 x 節點的父節點不是根,且 x 的父節點不是紅色
    while (x != null && x != root 
        && x.parent.color == RED) 
    { 
        //  x 的父節點是其父節點的左子節點
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) 
        { 
            // 獲取 x 的父節點的兄弟節點
            Entry<K,V> y = rightOf(parentOf(parentOf(x))); 
            // 如果 x 的父節點的兄弟節點是紅色(需要顏色翻轉)
            if (colorOf(y) == RED) 
            { 
                // 將 x 的父節點設為黑色
                setColor(parentOf(x), BLACK); 
                // 將 x 的父節點的兄弟節點設為黑色
                setColor(y, BLACK); 
                // 將 x 的父節點的父節點設為紅色
                setColor(parentOf(parentOf(x)), RED); 
                x = parentOf(parentOf(x)); 
            } 
            // 如果 x 的父節點的兄弟節點是黑色(需要旋轉)
            else 
            { 
                // 如果 x 是其父節點的右子節點
                if (x == rightOf(parentOf(x))) 
                { 
                    // 將 x 的父節點設為 x 
                    x = parentOf(x); 
                    rotateLeft(x); 
                } 
                // 把 x 的父節點設為黑色
                setColor(parentOf(x), BLACK); 
                // 把 x 的父節點的父節點設為紅色
                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; 
 }

關於刪除
如果理解了上面的插入操作的話,那么紅黑樹也已經理解了一半了,刪除操作也是比較難的一部分。它難在哪里呢?因為我們是刪除操作,但是我們又是繞到刪除的操作。

刪除的修復操作其實是一個不斷的上濾過程。這上濾過程跟添加的修復操作是一個道理。刪除操作表面上包含三種情況:
1、節點沒有兒子的
2、節點有一個兒子的
3、節點有兩個孩子的

我們隊紅黑樹刪除的操作,每一件事情都歸咎於能夠刪除樹葉。這是因為要刪除帶有兩個孩子節點的可以用右子樹的最小節點代替它。其他1和2情況直接刪除就可以了。
下面用一張圖片來說明,q(5)為要刪除的節點,q有兩個節點3和5,取最右節點的最左節點8和5的key和value交換,並把q指向節點8,那么現在的操作是不是變成刪除節點8了。
因為8是最右子樹的最左節點,所以它肯定沒有左孩子。最多只有一個右孩子。那么刪除的節點有兩個兒子,那么問題可以被轉化成刪除另一個只有一個兒子的節點的問題

如下面代碼的第9到12行,successor函數是找最右子樹的最小節點。但是刪除節點8肯定不會違反性質4(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點),但是如果8是黑色的,那么很肯能違反性質5,因為一條路徑上少了一個黑色節點。那么久只能修復了。紅黑樹的修復是一個上濾的過程,就是一直修復直到根節點(root)或者直到被替換的節點為紅色。下面的刪除操作deleteEntry。關鍵部分在修復函數fixAfterDeletion

private void deleteEntry(Entry<K,V> p) 
 { 
    modCount++; 
    size--; 
    // 如果被刪除節點的左子樹、右子樹都不為空
    if (p.left != null && p.right != null) 
    { 
        // 用 p 節點的中序后繼節點代替 p 節點
        Entry<K,V> s = successor (p); 
        p.key = s.key; 
        p.value = s.value; 
        p = s; 
    } 
    // 如果 p 節點的左節點存在,replacement 代表左節點;否則代表右節點。
    Entry<K,V> replacement = (p.left != null ? p.left : p.right); 
    if (replacement != null) 
    { 
        replacement.parent = p.parent; 
        // 如果 p 沒有父節點,則 replacemment 變成父節點
        if (p.parent == null) 
            root = replacement; 
        // 如果 p 節點是其父節點的左子節點
        else if (p == p.parent.left) 
            p.parent.left  = replacement; 
        // 如果 p 節點是其父節點的右子節點
        else 
            p.parent.right = replacement; 
        p.left = p.right = p.parent = null; 
        // 修復紅黑樹
        if (p.color == BLACK) 
            fixAfterDeletion(replacement);       // ①
    } 
    // 如果 p 節點沒有父節點
    else if (p.parent == null) 
    { 
        root = null; 
    } 
    else 
    { 
        if (p.color == BLACK) 
            // 修復紅黑樹
            fixAfterDeletion(p);                 // ②
        if (p.parent != null) 
        { 
            // 如果 p 是其父節點的左子節點
            if (p == p.parent.left) 
                p.parent.left = null; 
            // 如果 p 是其父節點的右子節點
            else if (p == p.parent.right) 
                p.parent.right = null; 
            p.parent = null; 
        } 
    } 
 }

下面是修復方法。上濾的過程中分為四種情況:
1、N的兄弟節點W為紅色
2、N的兄弟w是黑色的,且w的倆個孩子都是黑色的。
3、N的兄弟w是黑色的,w的左孩子是紅色,w的右孩子是黑色。
4、N的兄弟w是黑色的,且w的右孩子時紅色的。

其中第一種情況可以轉為2、3、4,其他三種也可以互相轉換

// 刪除節點后修復紅黑樹
 private void fixAfterDeletion(Entry<K,V> x) 
 { 
    // 直到 x 不是根節點,且 x 的顏色是黑色
    while (x != root && colorOf(x) == BLACK) 
    { 
        // 如果 x 是其父節點的左子節點
        if (x == leftOf(parentOf(x))) 
        { 
            // 獲取 x 節點的兄弟節點
            Entry<K,V> sib = rightOf(parentOf(x)); 
            // 如果 sib 節點是紅色
            if (colorOf(sib) == RED) 
            { 
                // 將 sib 節點設為黑色
                setColor(sib, BLACK); 
                // 將 x 的父節點設為紅色
                setColor(parentOf(x), RED); 
                rotateLeft(parentOf(x)); 
                // 再次將 sib 設為 x 的父節點的右子節點
                sib = rightOf(parentOf(x)); 
            } 
            // 如果 sib 的兩個子節點都是黑色
            if (colorOf(leftOf(sib)) == BLACK 
                && colorOf(rightOf(sib)) == BLACK) 
            { 
                // 將 sib 設為紅色
                setColor(sib, RED); 
                // 讓 x 等於 x 的父節點
                x = parentOf(x); 
            } 
            else 
            { 
                // 如果 sib 的只有右子節點是黑色
                if (colorOf(rightOf(sib)) == BLACK) 
                { 
                    // 將 sib 的左子節點也設為黑色
                    setColor(leftOf(sib), BLACK); 
                    // 將 sib 設為紅色
                    setColor(sib, RED); 
                    rotateRight(sib); 
                    sib = rightOf(parentOf(x)); 
                } 
                // 設置 sib 的顏色與 x 的父節點的顏色相同
                setColor(sib, colorOf(parentOf(x))); 
                // 將 x 的父節點設為黑色
                setColor(parentOf(x), BLACK); 
                // 將 sib 的右子節點設為黑色
                setColor(rightOf(sib), BLACK); 
                rotateLeft(parentOf(x)); 
                x = root; 
            } 
        } 
        // 下面是上面的對稱操作就不做具體分析了
        else 
        { 
            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); 
 }

參考
《數據結構與算法-java語言描述》
https://zh.wikipedia.org/wiki/紅黑樹#C.2B.2B.E7.A4.BA.E4.BE.8B.E4.BB.A3.E7.A0.81
https://www.ibm.com/developerworks/cn/java/j-lo-tree/
http://cmsblogs.com/?p=1013


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM