結合 TreeMap 源碼分析紅黑樹在 java 中的實現

csdn 連接:blog.csdn.net/ziwang_/art…java

注:本文的源碼摘自 jdk1.8 中 TreeMap數組

紅黑樹的意義


紅黑樹本質上是一種特殊的二叉查找樹,紅黑樹保證了一種平衡,插入、刪除、查找的最壞時間複雜度都爲 O(lgN)。那麼紅黑樹是如何實現這個特性的呢?紅黑樹區別於其餘二叉查找樹的規則在於它的每一個結點擁有紅色或黑色中的一種顏色,而後按照必定的規則組成紅黑樹,而這個規則就是咱們這篇文章所想要闡述的了。

紅黑樹的性質


紅黑樹遵循如下五點性質:

  • 性質1 結點是紅色或黑色。
  • 性質2 根結點是黑色。
  • 性質3 每一個葉子結點(NIL結點,空結點)是黑色的。
  • 性質4 每一個紅色結點的兩個子結點都是黑色。(從每一個葉子到根的全部路徑上不能有兩個連續的紅色結點)
  • 性質5 從任一結點到其每一個葉子結點的全部路徑都包含相同數目的黑色結點。

如下有幾個違反上述規則的結點示例:bash

違反性質1
違反性質1

結點必須是紅色或黑色學習

違反性質2
違反性質2

根結點必須是黑色的ui

違反性質3
違反性質3

葉子結點必須是黑色的spa

違反性質4
違反性質4

違反性質4
違反性質4

違反性質4
違反性質4

以上三個都是錯誤的紅黑樹示例,每一個紅色結點的兩個子結點都是黑色,而以下是合格的.net

遵循性質4
遵循性質4

固然,細心的讀者應該發現了我只是展現了前四條性質而沒有展現第五條性質,沒有什麼理由,筆者就是懶,第五條挺好理解的。翻譯

左旋、右旋


在學習紅黑樹以前想要介紹一個概念——左旋、右旋。這是一種結點操做,是紅黑樹裏面時常出現的一個操做,請看下圖 ——

左旋右旋概念圖
左旋右旋概念圖

這裏的左旋右旋都是針對根節點而言的,因此左圖到右圖是 y 結點右旋,右圖到左圖是 x 結點左旋。設計

  • 左旋:根結點退居右位,左子結點上位,同時左子結點的右子結點變成根節點左結點。
  • 右旋:根節點退居左位,右子節點上位,同時右子結點的左子結點變成根節點右結點。

如今不理解這倆概念有什麼用不重要,可是但願讀者能理解它的變幻過程,到後面會涉及到。3d

提及來枯燥無心,咱們能夠結合 TreeMap 來看看左旋右旋的源碼 ——

方法圖
方法圖

在這裏咱們就針對左旋源碼看看 ——

左旋源碼
左旋源碼

筆者就直接一行一行解釋吧:

private void rotateLeft(Entry<K,V> p) {
    if (p != null) {
        Entry<K,V> r = p.right;         // r 是根結點右子結點
        p.right = r.left;               // 爲根結點的左結點指向右子結點(也就是 r)的左結點
        if (r.left != null)
            r.left.parent = p;          // 意義同第二步,這步是右子結點(也就是 r)的左結點將父結點引用指向 p
        r.parent = p.parent;            // 將 r 結點的父引用指向 p 結點的父引用
        if (p.parent == null)
            root = r;                   // 將根結點替換爲 r
        else if (p.parent.left == p)
            p.parent.left = r;          // 意義同上
        else
            p.parent.right = r;         // 意義同上
        r.left = p;                     // r 左結點引用指向 p 結點
        p.parent = r;                   // p 結點父引用指向 r 結點
    }
}複製代碼


假設如今咱們找到了相應的結點插入位置,那麼咱們接下來就能夠插入相應的結點了,這個時候迎來一個頭疼的問題,咱們知道紅黑樹結點是有顏色的,那麼咱們應該給它設置成黑色的仍是紅色的呢?

設置成黑色的吧,就違反了性質5,設置成了紅色的吧,就容易違反了性質4。那怎麼辦?總要給一個顏色,那咱們就給紅色的吧。爲何?由於若是設置成黑色的話,該分支的黑色結點數量確定比其餘分支多一個,而這樣的話至關地很差作調整。若是將插入結點顏色置爲紅色的話,運氣比較好的狀況下該父結點就是黑色的,那這樣就不須要作任何調整。另外一種狀況是插入結點的父結點顏色是紅色的,這種狀況咱們就須要詳細討論了,具體分爲如下兩種(此處咱們以插入結點的父結點是爺爺結點的左子結點爲例(有點拗口),鏡像操做道理相同):

  • 1.父結點與叔叔結點都爲紅

父結點與叔叔結點都爲紅
父結點與叔叔結點都爲紅

父結點與叔叔結點都爲紅的話那麼一定爺爺結點爲黑,實際上此時咱們最簡單的操做就是將父結點和叔叔結點染黑,將爺爺結點染紅(將爺爺結點染紅的目的是爲了保證爺爺結點路徑的黑色結點數量不改變),以下 ——

染黑
染黑

如今目標結點、父結點、叔叔結點都符合要求了,可是爺爺結點的父結點是紅色的,那麼就衝突了,聰明的讀者可能已經發現了,此時的爺爺結點就至關於目標結點,咱們不妨將爺爺結點置換爲目標結點,再進行遞歸操做就能夠達到解決衝突的目的了。

  • 2.父結點爲紅,叔叔結點爲黑

父結點爲紅,叔叔結點爲黑
父結點爲紅,叔叔結點爲黑

但凡是有一個結點是紅色,那麼它的父結點一定是黑色(性質4),因此爺爺結點必定是黑色的。

有細心的小夥伴可能覺察到,上圖違反了性質五。實際上上圖是一張簡化後的圖,爲了咱們後面的內容更加便於理解,上圖的原圖應該是如下模樣 ——

上圖原圖
上圖原圖

ps:上圖中叔叔結點和兄弟結點能夠理解成 java 中的 null 結點,筆者特意將它們的個頭縮小了,以便區分。

那麼此時該怎麼操做呢?爺爺結點右旋,爺爺結點置紅,父結點置黑。這條操做事後,性質四、5都沒有違反。

爺爺結點右旋,爺爺結點置紅,父結點置黑
爺爺結點右旋,爺爺結點置紅,父結點置黑

固然,上圖也只是一張簡化圖,實際上原圖以下:

上圖原圖
上圖原圖

那麼結合 TreeMap 源碼咱們來看看:

插入調整源碼
插入調整源碼

翻譯以下:

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)));  // y 是叔叔結點
            // 狀況1 叔叔結點也爲紅
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);               // 父結點賦黑
                setColor(y, BLACK);                         // 叔叔結點賦黑
                setColor(parentOf(parentOf(x)), RED);       // 爺爺結點賦紅
                x = parentOf(parentOf(x));                  // 爺爺結點置爲目標結點,遞歸
            } else {
                // 狀況2 叔叔結點爲黑
                // 小插曲,若是目標結點是父結點的右子結點,左旋父結點
                // 固然,此時目標結點應改成父結點
                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;     // 根結點必須賦黑
}複製代碼

看完代碼咱們發現咱們好像漏了一個小插曲(固然,這是筆者故意的),那麼小插曲是一個什麼狀況呢?言語來講,在叔叔結點爲黑的前提下,當目標結點是父結點的右子結點的時候,須要對父結點進行左旋而後才能接續下一步操做,爲何會這樣,咱們一圖勝千言 ——

小插曲
小插曲

若是忽略上述狀況,那麼最終會獲得如下狀況:

小插曲忽略狀況下實現
小插曲忽略狀況下實現

因爲目標結點是父結點的右子節點,在爺爺結點右旋過程當中,它會轉爲原爺爺結點的左子結點,這樣的話就違反了特性4和特性5。解決方法就是上面所提到的將父結點先進行左旋而後再進行前面所提到的操做,以下圖 ——

小插曲修正
小插曲修正

固然,不要忘了,如今須要調整的結點是原父結點,也就是要將上圖左下角那個結點做爲目標結點進行調整。

因此紅黑樹的添操做分爲如下三步:

  • 找到相應的插入位置
  • 將目標結點設置爲紅色並插入
  • 經過着色和旋轉等操做使之從新成爲一棵二叉樹


這一小節我想先 show 出源碼再來解釋 ——

刪除結點源碼
刪除結點源碼

翻譯以下:

private void deleteEntry(Entry<K,V> p) {
    // 優先選擇左子結點做爲被刪結點的替代結點
    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;

        // 將刪除結點的各個引用置 null
        p.left = p.right = p.parent = null;

        // 若是刪除結點顏色爲黑色,那麼須要進行刪後調整
        if (p.color == BLACK)
            fixAfterDeletion(replacement);
    } else if (p.parent == null) {
        // 若是替代結點爲空且刪除結點爲 root 結點
        root = null;
    } else {
        // 若是刪除結點爲空且不是 root 結點
        // 若是刪除結點顏色爲黑色,那麼須要進行刪後調整
        if (p.color == BLACK)
            fixAfterDeletion(p);

        // 將刪除結點的各個引用置 null
        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;
        }
    }
}複製代碼

刪除時可能分爲三種狀況,具體的作法也在上述代碼中作了清晰的解釋,筆者在此就不擴展了,細心的讀者可能發現了,上述刪除操做凡是涉及到了刪除結點是黑色的狀況下,都須要調用 fixAfterDeletion() 方法對紅黑樹進行調整。這是由於若是刪除結點是黑色的,當它被刪除後就會違反性質5,因此咱們須要對紅黑樹進行結構調整。

爲了便於理解紅色結點爲何不會影響紅黑樹總體結構,筆者仍是舉了一個例子給各位讀者理解一下,下圖是刪除前:

刪除前
刪除前

下圖是刪除後:

刪除後
刪除後

實際上紅黑樹是使用如下2點思想來進行調整的(筆者認爲,在分析 fixAfterDeletion() 代碼實現以前,做爲開發者應該去自行思考一下若是咱們做爲源碼設計者,咱們會如何來解決這個問題。) ——

1.給刪除結點的路徑增長一個黑色結點(將兄弟路徑的一個黑色結點移過來)
2.給刪除結點的兄弟路徑減小一個黑色結點(將兄弟路徑的一個紅色結點染黑)

ps:後面咱們會針對第一條稱爲思想1,第二條稱爲思想2

說完思想,咱們討論一下具體刪除操做是如何進行的。紅黑樹在保障刪除結點的兄弟結點爲黑色的狀況下(沒有什麼特殊原因,僅僅是爲了後期好操做),分如下兩點來進行分析:

1.兄弟結點的兩個子結點都是黑色的
2.另外一種狀況(兄弟結點的兩個子結點至多一個黑色的)

ps:後面咱們會針對第一條稱爲狀況1,第二條稱爲狀況2

對於狀況1來講,紅黑樹採用思想2,將兄弟結點置爲紅色,可是這樣帶來了兩個問題——對於父路徑來講,它與兄弟路徑黑色結點數量不一樣,違反性質5;且若是父結點也是紅色,那麼它勢必與孩子結點衝突,還會違反性質4,以下圖——

下圖示例違反性質5:

原圖
原圖

違反性質5
違反性質5

下圖示例違反性質5且違反性質4:

原圖
原圖

違反性質四、5
違反性質四、5

對於前一個問題用遞歸的思想來解決,將父親結點置爲目標結點,讓父親結點的兄弟結點也要減小一個黑色結點就能夠了(借鑑思想2);而對於後一個問題,只須要將父結點置黑便可(借鑑思想2)。jdk 中相關實現源碼以下:

while (x != root && colorOf(x) == BLACK) {
    Entry<K,V> sib = rightOf(parentOf(x));
    if (colorOf(leftOf(sib))  == BLACK &&
        colorOf(rightOf(sib)) == BLACK) {
        setColor(sib, RED);
        x = parentOf(x);
    }
}

setColor(x, BLACK);複製代碼

前面闡述的是針對狀況1而言,針對於狀況2而言,紅黑樹採用的是思想1,具體作法分爲又得分爲如下兩種小狀況:

  • 兄弟結點的右子結點不爲黑
  • 兄弟結點的右子結點爲黑

對於第一種小狀況,紅黑樹採用如下操做:

1.兄弟結點置父結點顏色(準備謀權篡位)
2.父結點置黑、兄弟結點右結點置黑
3.父結點左旋

該思想不只保證了更新結點後不會衝突(父結點與兄弟結點不衝突,兄弟結點與右子結點不衝突,兄弟結點左子結點與父結點不衝突),而且保證了黑色結點數量不會改變,一圖勝千言——

第一種小狀況原圖
第一種小狀況原圖

第一種小狀況刪除後修正
第一種小狀況刪除後修正

jdk 中相關源碼以下:

while (x != root && colorOf(x) == BLACK) {
    setColor(sib, colorOf(parentOf(x)));
    setColor(parentOf(x), BLACK);
    setColor(rightOf(sib), BLACK);
    rotateLeft(parentOf(x));
    x = root;
}

setColor(x, BLACK);複製代碼

而對於第二種小狀況,紅黑樹採用如下操做:

1.將兄弟結點的左子結點染黑
2.兄弟結點染紅
3.兄弟結點右旋

第二種小狀況原圖
第二種小狀況原圖

第二種小狀況刪除後修正
第二種小狀況刪除後修正

實際上細心的讀者發現了,轉換後的結構是等同於第一種小狀況的初始結構,因此接下來就按照第一種小狀況的步驟去變換結構,相關源碼以下:

while (x != root && colorOf(x) == BLACK) {
    if (colorOf(rightOf(sib)) == BLACK) {   // 狀況2
        setColor(leftOf(sib), BLACK);
        setColor(sib, RED);
        rotateRight(sib);
        sib = rightOf(parentOf(x));
    }

    // 狀況1
    setColor(sib, colorOf(parentOf(x)));
    setColor(parentOf(x), BLACK);
    setColor(rightOf(sib), BLACK);
    rotateLeft(parentOf(x));
    x = root;
}

setColor(x, BLACK);複製代碼

這一塊可能有一些複雜,但記住如下三點核心思想問題就不是很大了:

  • 父結點替換刪除結點(保障了刪除結點路徑上的黑色結點數量不變)
  • 兄弟結點替換父結點(保障了父結點路徑上的黑色結點數量不變)
  • 右子結點(結構變化前必定是紅色的,變換後置黑)替換兄弟結點(保障了兄弟路徑上的黑色結點數量不變)

那麼接下來就是看看 fixAfterDeletion() 的代碼實現了 ——

結點刪除調整源碼
結點刪除調整源碼

解釋以下:

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

            // 小插曲1,若是兄弟結點爲紅
            // 這步是保障兄弟結點必定爲黑
            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 {
                // 兄弟結點的子結點至多一個是黑色的

                // 小插曲2,兄弟結點左子結點爲紅,右子結點爲黑的狀況
                // 這步的意義是讓兄弟結點的右子結點的數量多一個
                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 { // 鏡像操做
            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 中的運用實際上仍是挺多的,例如 TreeSet 的默認底層實現實際上也是 TreeMap;jdk 8中的 HashMap 實現也由原來的數組+鏈表更改成了數組+鏈表/紅黑樹。

相關文章
相關標籤/搜索