Java集合——TreeMap(二)

午夜12點發表於2018-05-12

簡述

在上篇中圍繞原始碼講了TreeMap的一些方法,本文主要闡述其如何維持特性:
①.節點是紅色或黑色
②.根節點是黑色
③.每個葉節點(NIL節點,空節點)是黑色的
④.每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點)
⑤.從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點
注意:
性質1和性質3總是保持著
性質4只在增加紅色節點、重繪黑色節點為紅色,或做旋轉時受到威脅
性質5只在增加黑色節點、重繪紅色節點為黑色,或做旋轉時受到威脅

新增平衡


    private void fixAfterInsertion(Entry x) {
        // 新插入的節點預設紅色
        x.color = RED;
        
        while (x != null && x != root && x.parent.color == RED) {
            // 若x的父節點是其父節點的父節點的左子樹
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                //獲取x父節點的父節點的右子樹y
                Entry y = rightOf(parentOf(parentOf(x)));
                //若y顏色為紅色
                if (colorOf(y) == RED) {
                    //x父節點的顏色設為黑色
                    setColor(parentOf(x), BLACK);
                    //y的顏色設為黑色
                    setColor(y, BLACK);
                    //x父節點的父節點顏色設為紅色
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    //若x為其父節點的右子樹
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        //左旋轉
                        rotateLeft(x);
                    }
                    //x父節點的顏色設為黑色
                    setColor(parentOf(x), BLACK);
                    //x父節點的父節點顏色設為紅色
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
                //獲取x父節點的父節點的左子樹y
                Entry y = leftOf(parentOf(parentOf(x)));
                //若y顏色為紅色
                if (colorOf(y) == RED) {
                    //x父節點設為黑色
                    setColor(parentOf(x), BLACK);
                    //y顏色設為黑色
                    setColor(y, BLACK);
                    //x父節點的父節點顏色設為紅色
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                //若y顏色為黑色    
                } else {
                    //若x為父節點的左子樹
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        //右旋轉
                        rotateRight(x);
                    }
                    //x父節點顏色設為黑色
                    setColor(parentOf(x), BLACK);
                    //x父節點的父節點顏色設為紅色
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
        //根節點必須為黑色
        root.color = BLACK;
    } 
複製程式碼

新增有三點:
①.新增結點顏色為紅色(方法中第一行)
②.若新增結點的父節點顏色是黑色,可以維持性質
③.若有任何紅黑性質被破壞,則至多隻有一條被破壞,或是性質2,或是性質4。若性質2被破壞,其原因為新增結點是根節點且顏色為紅,若性質4被破壞,其原因為新增結點與其父節點顏色都為紅色

場景一:新增節點為根節點

此情形不會呼叫到fixAfterInsertion方法,在put方法中直接通過Entry類構造方法建立根節點,建立出的結點預設黑色,符合紅黑樹性質無需平衡。
put方法部分程式碼:


public V put(K key, V value) {
    Entry t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check
            //鍵,值,父節點
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        } 
        .
        .
        .
}
複製程式碼

場景二:新增節點的父節點顏色為黑色

此情況也無需平衡不會走進fixAfterInsertion方法迴圈體內,因為新增節點總是紅色,又因為性質4其會有兩個黑色NIL節點,所以通過它的每個子節點的路徑依然包含相同數目的黑色節點滿足性質5。
例項如圖新增節點12:

Java集合——TreeMap(二)
插入後:
Java集合——TreeMap(二)
若節點12為黑色如圖,我們可以發現結點11至其葉子節點數目明顯不同:
Java集合——TreeMap(二)

場景三:新增節點的父節點顏色與其叔父節點(一個節點的父節點的兄弟節點)都是紅色

如圖新增節點19,我們可以發現其違反了性質4(每個紅色節點的兩個子節點都是黑色)。

Java集合——TreeMap(二)
結合程式碼


    if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                //獲取x的叔父節點
                Entry y = rightOf(parentOf(parentOf(x)));
                //若y顏色為紅色
                if (colorOf(y) == RED) {
                    //x父節點的顏色設為黑色
                    setColor(parentOf(x), BLACK);
                    //y的顏色設為黑色
                    setColor(y, BLACK);
                    //x父節點的父節點顏色設為紅色
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                }
    }            
複製程式碼

先將父節點與叔父節點顏色改為黑色,祖父結點顏色改為紅色,結果如圖:

Java集合——TreeMap(二)
我們發現祖父結點25與其父節點17又違反了性質4再次相同操作,將父節點與叔父節點顏色改為黑色,祖父結點顏色改為紅色,最後執行最後一行程式碼,根節點改黑色,結果如圖:

Java集合——TreeMap(二)

場景四:新增節點父節點是紅色而叔父節點是黑色或缺少,並且新節點是其父節點的右子樹而父節點又是其父節點的左子樹

例項如圖新增節點15:

Java集合——TreeMap(二)
結合程式碼先進行一次左旋轉,p為圖中節點14:


    if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                Entry y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    .
                    .
                    .
                } else {
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            }
    }        
    
    /**
     * 左旋轉
     */
    private void rotateLeft(Entry p) {
        if (p != null) {
            //獲取p的右子樹r
            Entry r = p.right;
            //將r的左子樹設為p的右子樹
            p.right = r.left;
            //若r的左子樹不為空,則將p設為r左子樹的父節點
            if (r.left != null)
                r.left.parent = p;
            //將p的父節點設為r的父節點    
            r.parent = p.parent;
            //若p的父節點為空,則r設為根節點
            if (p.parent == null)
                root = r;
            //若p為其父節點左子樹,則將r設為p父節點左子樹    
            else if (p.parent.left == p)
                p.parent.left = r;
            //否則r設為p的父節點的右子樹
            else
                p.parent.right = r;
            將p設為r的左子樹    
            r.left = p;
            將r設為p的父節點
            p.parent = r;
        }
    }  
複製程式碼

左旋轉動態圖如下:

Java集合——TreeMap(二)
經過左旋轉後:
Java集合——TreeMap(二)
隨後將x(節點14)父節點顏色改為黑色,祖父節點顏色改為紅色,以祖父節點進行右旋轉


    /**
     * 右旋轉
     */
    private void rotateRight(Entry p) {
        if (p != null) {
            //獲取p的左子樹l
            Entry l = p.left;
            將l的右子樹設為p的左子樹
            p.left = l.right;
            //若l的右子樹不為空,則將p設為l的右子樹的父節點
            if (l.right != null)
                l.right.parent = p;
            //將p的父節點設為l的父節點
            l.parent = p.parent;
            //若p的父節點為空,則將l設為根節點
            if (p.parent == null)
                root = l;
            //若p為p父節點的右子樹,則將l設定為p的父節點的右子樹    
            else if (p.parent.right == p)
                p.parent.right = l;
            //否則將l設為p的父節點的左子樹    
            else p.parent.left = l;
            //將p設為l的右子樹
            l.right = p;
            //將l設為p的父節點
            p.parent = l;
        }
    } 
複製程式碼

右旋轉動態圖如下:

Java集合——TreeMap(二)
經過這些處理後,符合紅黑樹性質:
Java集合——TreeMap(二)

場景五:新增節點父節點是紅色,而叔父節點是黑色或缺少,新節點是其父節點的左子樹,而父節點又是其父節點的左子樹
例項如圖新增節點14:

Java集合——TreeMap(二)
結合程式碼因為是父節點左子樹,所有不需要左旋轉,直接將其父節點改黑,祖父節點改紅,以祖父節點右旋轉:


    if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
                Entry y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    .
                    .
                    .
                } else {
                    if (x == rightOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
    }  
複製程式碼

操作後如圖,符合紅黑樹性質:

Java集合——TreeMap(二)

刪除平衡

刪除平衡原始碼:


    private void fixAfterDeletion(Entry x) {
        while (x != root && colorOf(x) == BLACK) {
            //若x為其父節點的左子樹
            if (x == leftOf(parentOf(x))) {
                //獲取x兄弟節點 
                Entry 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;
                }
            //若x為其父節點的右子樹    
            } else { // symmetric
                Entry 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);
    } 
複製程式碼

在上篇文章所提到刪除節點的情況:
①.葉子結點:直接將其父節點對應孩子置空,若刪除左葉子結點則將其父結點左子樹置空,若刪除右葉子結點則將其父結點右子樹置空
②.一個孩子:用子節點替代需刪除節點
③.兩個孩子:用後繼節點替換待刪除節點,然後刪除後繼節點

場景一:刪除葉子節點

1.若葉子節點為紅色,如圖刪除節點12

Java集合——TreeMap(二)
刪除後:

Java集合——TreeMap(二)
此情況無需平衡,結合deleteEntry方法其不會呼叫到fixAfterDeletion方法

2.1.若葉子節點為黑色左子樹且其兄弟節點(黑色)的兩個子節點都為黑色或無孩子

如上圖刪除節點1,結合程式碼,將節點1兄弟節點11改紅色,再最後將其父節點8改紅色:


    //sib兄弟節點(這裡只節點11)
    if (colorOf(leftOf(sib))  == BLACK &&
                    colorOf(rightOf(sib)) == BLACK) {
                    //將兄弟節點改為紅色 
                    setColor(sib, RED);
                    //刪除的節點(即節點1)指向父節點(節點8)
                    x = parentOf(x);
                }
    }      
    .
    .
    .
    setColor(x, BLACK);
複製程式碼

Java集合——TreeMap(二)

2.2.若葉子節點為黑色左子樹且其兄弟節點(黑色)左子樹為紅色,右子樹為黑色

如圖刪除節點1:

Java集合——TreeMap(二)


    Entry sib = rightOf(parentOf(x));
    .
    .
    .
     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;
    .
    .
    .
    setColor(x, BLACK);
複製程式碼

此場景走此原始碼分支程式碼,兄弟節點左子樹改(節點9)為黑色,兄弟節點(節點11)改為紅色,以兄弟節點右旋轉,再將其指向旋轉後的兄弟節點,if分支程式碼走完,操作後如圖:

Java集合——TreeMap(二)

再將旋轉後的兄弟節點(節點9)顏色改為父節點(節點8)顏色(紅色),將父節點改為黑色,將兄弟節點右子樹(節點11)改為黑色,以父節點左轉,最後x指向root退出迴圈且執行最後一行確保根節點黑色

Java集合——TreeMap(二)

2.3.若葉子節點為黑色左子樹且其兄弟節點(黑色)右子樹為紅色,左子樹為黑色:

此情況就是不走上述if分支其餘一樣不再過多寫了

2.4.若葉子節點為黑色左子樹且其兄弟節點(紅色):

如圖刪除節點11:

Java集合——TreeMap(二)
走如下程式碼:


    if (x == leftOf(parentOf(x))) {
                Entry 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;
                }
    }
    setColor(x, BLACK);
複製程式碼

將兄弟節點(節點15)改為黑色,父節點(節點13)改為紅色,以父節點左旋轉,旋轉後:

Java集合——TreeMap(二)
此時兄弟節點變為節點14兩孩子都為黑色,進入此分支程式碼將兄弟節點改為紅色,x(節點11)指向父節點,將其顏色改為黑色

Java集合——TreeMap(二)

注意這裡只列了兄弟節點紅色且其兩個孩子都為黑色或無孩子,其他情況可以轉換成2.2,2.3

場景二:刪除一個孩子節點

此場景用子節點替代需刪除節點,平衡較為簡單。

Java集合——TreeMap(二)

如上圖刪除節點17(紅色),只需將根節點右子樹設為節點15,紅黑樹性質依舊保持無需平衡
刪除節點8(黑色),將節點11左子樹設為節點1,因為節點1,11都為紅色違反性質4(每個紅色節點的兩個子節點都是黑色),進行調色平衡節點1改為黑色即可,刪除後

Java集合——TreeMap(二)

場景三:刪除兩個孩子節點

1.刪除節點的後繼為紅色

此情況可以轉換成場景一中1處理

Java集合——TreeMap(二)

2.1.刪除節點後繼節點為黑色,後繼節點的兄弟節點為黑色且其兩個子節點都為黑色或無孩子

可以轉換成場景一中2.1處理

2.2.刪除節點後繼節點為黑色,後繼節點的兄弟節點為黑色且其左子樹為紅色,右子樹為黑色

可以轉換成場景一中2.2處理

2.3.刪除節點後繼節點為黑色,後繼節點的兄弟節點為黑色且其左子樹為黑色,右子樹為紅色

可以轉換成場景一中2.3處理

2.4 刪除節點後繼節點為黑色,後繼節點的兄弟節點為紅色

可以轉換成場景一中2.4處理

總結

本文列舉TreeMap插入刪除平衡操作結合原始碼闡述其原理,若有錯誤望指正。。。

參考

維基百科——紅黑樹

相關文章