Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

knock_小新發表於2019-03-03

引言

說完了二叉樹的遍歷、新增和刪除引申到了紅黑樹的遍歷、新增和刪除。對二叉樹結構有了一定的瞭解,在這篇文章中將會對紅黑樹性質進行詳細的說明。

紅黑樹

二叉樹在理想情況下時間複雜度是O(logn),最壞情況下當插入的資料由小到大或者由大到小排列的時候,二叉樹就變成了一個連結串列,而我們知道連結串列檢索的時間複雜度是O(n),效率非常差,所以出現了AVL樹和紅黑樹來改變這種狀況。同時由於AVL樹的極端平衡特性,導致新增和刪除資料後需要過多的旋轉操作來保證AVL樹平衡的特徵,所以TreeMap中會使用紅黑樹來儲存資料。
最好情況下的二叉樹:

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

最差情況下的二叉樹:

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

樹旋轉

當AVL樹在新增或者刪除節點時出現不平衡後通過什麼操作來保證樹的平衡性呢?這種操作就叫做書旋轉。紅黑樹也是如此,不過紅黑樹更復雜,這點我們後面再說,先來看看AVL樹的旋轉操作。
樹旋轉操作是由於二叉樹在新增節點時為了避免出現平衡失效的情況而做的一種操作,操作的基本原則是操作後不影響二叉樹中序遍歷的結果。
這裡我們用AVL樹來說明這個問題。AVL樹是一種高度平衡的二叉樹,他的任何兩個節點的子樹的高度最大差別為1,這樣他的查詢、插入和刪除的時間複雜度都是O(logn),當出現不平衡情況的時候,就需要執行樹旋轉。

旋轉操作

樹的旋轉操作分為兩種,左旋轉和右旋轉,這兩種旋轉是相對的。通過右旋或者左旋操作我們可以使一棵樹繼續保持平衡狀態,並不會改變中序遍歷的結果,但同時也要付出相應的代價。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)
//右旋
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;
    }
}
//左旋
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;
    }
}
複製程式碼

AVL樹旋轉的幾種情況

當樹的任何兩個節點的子樹的高度差大於1的時候,就需要進行旋轉以保證任何兩個節點的子樹的高度最大差別為1。哪幾種情況下需要進行樹旋轉操作?
1.左左情況,左節點比右節點多兩個節點,並且多出的節點都在左子樹;

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

2.右右情況,右節點比左節點多兩個節點,並且多出的節點都在右子樹;

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

3.左右情況,左節點或者右節點多出兩個節點,多出的第一個節點在左子樹,第二個節點在右子樹;

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

4.右左情況,左節點或者右節點多出兩個節點,多出的第一個節點在右子樹,第二個節點在左子樹;

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

紅黑樹的特性

明白了AVL樹的旋轉操作,再來看紅黑樹就簡單多了,紅黑樹就是一顆滿足一定條件的,相對平衡的二叉樹,是二叉樹和AVL樹的一種折中。
紅黑樹的新增刪除操作同二叉樹一樣,但是當新增刪除等操作後使紅黑樹失去了他的特性後,就需要進行旋轉操作來恢復紅黑樹的特性。
紅黑樹需要滿足以下幾點性質:
1.每個節點要麼是紅色,要麼是黑色。
2.根節點永遠是黑色的。
3.所有的葉節點都是空節點(即 null),並且是黑色的。
4.每個紅色節點的兩個子節點都是黑色。(從每個葉子到根的路徑上不會有兩個連續的紅色節點)
5.從任一節點到其子樹中每個葉子節點的路徑都包含相同數量的黑色節點。
性質1和性質2很好理解。性質3在Java裡面指的是空節點,一般不用考慮。性質4保證了從根到葉子節點的最長路徑最多隻能是最短路徑的兩倍,根據性質5建立一顆黑色節點為3的紅黑樹,最短路徑為黑-黑-黑,最長路徑為黑-紅-黑-紅-黑-紅(因為每個紅色節點的兩個子節點都是黑色,紅色則不可連續)。
下圖是一顆標準的紅黑樹:

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

紅黑樹新增後修復

在上一篇文章中的新增操作後呼叫了fixAfterInsertion(e)方法用來修復被破壞的紅黑樹性質。
一般預設每個新增的節點都為紅色,因為新增的節點如果為黑色,那就一定會破壞性質5,而且很難修復。但如果新增的是紅色節點,有可能就不會破壞任何性質,也可能會破壞性質4導致連續的紅色節點,可以通過變色和旋轉來修復。
在新增紅色節點時可能會遇到以下幾種情況:
1.新節點為根節點,父節點為空,破壞性質2,修復紅色為黑色即可。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

2.新節點的父節點為黑色,新增的新節點為紅色,不破壞任何性質。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

3.新節點的父節點為紅色,同時父節點的兄弟節點也為紅色(根據性質4,父節點的父節點為黑色),新增的新節點也為紅色,破壞了性質4,修復父節點和父節點的兄弟節點為黑色,同時父節點的父節點為紅色,保證性質5不被破壞。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

4.新節點的父節點為紅色,同時父節點的兄弟節點為黑色或為空(空也為黑色)。如果新節點為父節點的左節點,但新節點的父節點為祖父節點的右節點;或者新節點為父節點的右節點,但新節點的父節點為祖父節點的左節點,就需要先右旋或者左旋,然後轉換成情況5,再進行一次著色和旋轉。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

5.新節點的父節點為紅色,同時父節點的兄弟節點為黑色或為空(空也為黑色)。如果新節點為父節點的左節點,同時新節點的父節點也為祖父節點的左節點;或者新節點為父節點的右節點,同時新節點的父節點也為祖父節點的右節點。設定新節點的父節點為黑色,設定新節點的祖父節點為紅色,然後左旋或者右旋即可。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED;
    //情況2 x.parent.color == BLACK
    while (x != null && x != root && x.parent.color == RED) {
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            //情況3 父節點的兄弟節點也為紅色 不需要旋轉
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //情況4 父節點的兄弟節點為黑色 父節點為祖父節點的左節點 x為父節點的右節點 先左旋變為情況5
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateLeft(x);
                }
                //情況5 父節點的兄弟節點為黑色 父節點為祖父節點的左節點 x也為父節點的左節點 改變父節點為黑色 祖父節點為紅色 然後祖父節點右旋
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            //情況3
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            } else {
                //情況4 父節點的兄弟節點為黑色 父節點為祖父節點的右節點 x為父節點的左節點 先右旋變為情況5
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);
                    rotateRight(x);
                }
                //情況5 父節點的兄弟節點為黑色 父節點為祖父節點的右節點 x也為父節點的右節點 改變父節點為黑色 祖父節點為紅色 然後祖父節點左旋
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    //情況1
    root.color = BLACK;
}
複製程式碼

紅黑樹刪除後修復

紅黑樹在刪除後呼叫了fixAfterDeletion(p)用來修復被破壞的紅黑樹性質。
由於在刪除時我們採用後繼節點替換的方法,替換之後只需要刪除替換的節點即可。這樣刪除節點的問題就可以轉換為刪除至多隻有1個孩子節點的問題(因為後繼節點至多隻有右孩子,或者沒有孩子)。
刪除時有以下幾種情況:
1.刪除的節點為紅色,根據紅色節點不可連續,則他的父節點和子節點都為黑色,直接用他的黑色子節點替換即可,刪除了紅色節點不會破壞性質5。
2.刪除的節點為黑色,但是兒子為紅色,直接用紅色節點替換,替換後變紅色節點為黑色即可。
3.刪除的節點為黑色,同時兒子也為黑色,這種情況比較複雜,又可以分為幾種情況:
將兒子命名為S,兒子替換後的父親命名為F,兒子替換後的兄弟命名為B,兄弟的左節點命名為BL,兄弟的右節點命名為BR,情況3又可以分為以下幾種情況:
4.F為任意色,B為紅色,將F左旋轉,並交換F和B的顏色,則通過各自路徑的黑色節點數目不變,但S現在有了一個紅色的父節點F,一個黑色的兄弟節點B,則情況可以變成5、6或者7。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

5.F為任意色,B為黑色,BL和BR也為黑色,只需要將B的顏色設定為紅色,則通過B的路徑少了一個黑色節點和通過S的黑色節點相等了,但通過F的路徑少了一個黑色節點,可以重新從第一種情況進行迭代。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

6.F為任意色,B為黑色,BL為紅色,BR為黑色,將B右旋,這樣就變成了情況7。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)

7.F為任意色,B為黑色,BL為黑色,BR為紅色,將F左旋,同時交換F和B和顏色即可。

Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)
private void fixAfterDeletion(Entry<K,V> x) {
    while (x != root && colorOf(x) == BLACK) {
        if (x == leftOf(parentOf(x))) {
            Entry<K,V> sib = rightOf(parentOf(x));
            //情況4 F為任意色,B為紅色 情況可以變成5、6或者7
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateLeft(parentOf(x));
                sib = rightOf(parentOf(x));
            }
            //情況5 F為任意色,B為黑色,BL和BR也為黑色
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                //情況6 F為任意色,B為黑色,BL為紅色,BR為黑色
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateRight(sib);
                    sib = rightOf(parentOf(x));
                }
                //情況7 F為任意色,B為黑色,BL為黑色,BR為紅色
                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));
            //情況4 F為任意色,B為紅色 情況可以變成5、6或者7
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                setColor(parentOf(x), RED);
                rotateRight(parentOf(x));
                sib = leftOf(parentOf(x));
            }
            //情況5 F為任意色,B為黑色,BL和BR也為黑色
            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);
                x = parentOf(x);
            } else {
                //情況6 F為任意色,B為黑色,BL為紅色,BR為黑色 旋轉著色後變為情況7
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);
                    setColor(sib, RED);
                    rotateLeft(sib);
                    sib = leftOf(parentOf(x));
                }
                //情況7 F為任意色,B為黑色,BL為黑色,BR為紅色
                setColor(sib, colorOf(parentOf(x)));
                setColor(parentOf(x), BLACK);
                setColor(leftOf(sib), BLACK);
                rotateRight(parentOf(x));
                x = root;
            }
        }
    }

    setColor(x, BLACK);
}
複製程式碼

總結

理解了紅黑樹遍歷,刪除新增等操作的分析,再理解TreeMap<K,V>實現邏輯就會很容易。TreeMap<K,V>所有的操作都是在紅黑樹的基礎上執行的,紅黑樹的每一個節點對應為TreeMap<K,V>的一個Entry<K,V>。
TreeSet<E>由於在實現上完全使用了TreeMap<K,V>的key來實現,所以TreeSet<E>的所有操作一樣是建立在紅黑樹的基礎上。

相關文章