1. 程式人生 > >資料結構和演算法 | 紅黑樹演算法(一)

資料結構和演算法 | 紅黑樹演算法(一)

1 紅黑樹簡介

紅黑樹是一種自平衡的二叉查詢樹,是一種高效的查詢樹。它是由 Rudolf Bayer 於1978年發明,在當時被稱為對稱二叉 B 樹(symmetric binary B-trees)。後來,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改為如今的紅黑樹。紅黑樹具有良好的效率,它可在 O(logN) 時間內完成查詢、增加、刪除等操作。因此,紅黑樹在業界應用很廣泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基於紅黑樹結構實現的。考慮到紅黑樹是一種被廣泛應用的資料結構,所以我們很有必要去弄懂它。

2 紅黑樹的性質

學過二叉查詢樹的同學都知道,普通的二叉查詢樹在極端情況下可退化成連結串列,此時的增刪查效率都會比較低下。為了避免這種情況,就出現了一些自平衡的查詢樹,比如 AVL紅黑樹等。這些自平衡的查詢樹通過定義一些性質,將任意節點的左右子樹高度差控制在規定範圍內,以達到平衡狀態。以紅黑樹為例,紅黑樹通過如下的性質定義實現自平衡:

  1. 樹中的每個結點顏色不是紅的,就是黑的;
  2. 根結點的顏色是黑的;
  3. 所有葉子都是黑色(葉子是NIL節點);
  4. 如果此結點是紅的,那麼它的兩個孩子結點全部都是黑的(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點);
  5. 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點(簡稱黑高度)。

有了上面的幾個性質作為限制,即可避免二叉查詢樹退化成單鏈表的情況。但是,僅僅避免這種情況還不夠,這裡還要考慮某個節點到其每個葉子節點路徑長度的問題。如果某些路徑長度過長,那麼,在對這些路徑上的及誒單進行增刪查操作時,效率也會大大降低。這個時候性質4和性質5用途就凸顯了,有了這兩個性質作為約束,即可保證任意節點到其每個葉子節點路徑最長不會超過最短路徑的2倍。原因如下:

當某條路徑最短時,這條路徑必然都是由黑色節點構成。當某條路徑長度最長時,這條路徑必然是由紅色和黑色節點相間構成(性質4限定了不能出現兩個連續的紅色節點)。而性質5又限定了從任一節點到其每個葉子節點的所有路徑必須包含相同數量的黑色節點。此時,在路徑最長的情況下,路徑上紅色節點數量 = 黑色節點數量。該路徑長度為兩倍黑色節點數量,也就是最短路徑長度的2倍。舉例說明一下,請看下圖:

上圖畫出了從根節點 M 出發的到其葉子節點的最長和最短路徑。這裡偷懶只畫出了兩條最長路徑,實際上最長路徑有4條,分別為:
  • M -> Q -> O -> N
  • M -> Q -> O -> p
  • M -> Q -> Y -> X
  • M -> Q -> Y -> Z

長度為4,最短路徑為 M -> E,長度為2。最長路徑的長度正好為最短路徑長度的2倍。

前面說了關於紅黑樹的一些性質,這裡還需要補充一些其他方面的東西。在紅黑樹簡介一節中說到紅黑樹被髮明出來的時候並不叫紅黑樹,而是叫做對稱二叉 B 樹,從名字中可發現紅黑樹和 B 樹(這裡指的是2-3樹)或許有一定的關聯,事實也正是如此。如果對紅黑樹的性質稍加修改,就能讓紅黑樹和B樹形成一一對應的關係。關於紅黑樹和 B 樹關係的細節這裡不展開說明了,有興趣的同學可以參考《演算法》第4版,那本書上講的很透徹。

3 紅黑樹操作

紅黑樹的基本操作和其他樹形結構一樣,一般都包括查詢、插入、刪除等操作。前面說到,紅黑樹是一種自平衡的二叉查詢樹,既然是二叉查詢樹的一種,那麼查詢過程和二叉查詢樹一樣,比較簡單,這裡不再贅述。相對於查詢操作,紅黑樹的插入和刪除操作就要複雜的多。尤其是刪除操作,要處理的情況比較多,不過大家如果靜下心來去看,會發現其實也沒想的那麼難。好了,廢話就說到這,接下來步入正題吧。

3.1 旋轉操作

在分析插入和刪除操作前,這裡需要插個隊,先說明一下旋轉操作,這個操作在後續操作中都會用得到。旋轉操作分為左旋和右旋,左旋是將某個節點旋轉為其右孩子的左孩子,而右旋是節點旋轉為其左孩子的右孩子。這話聽起來有點繞,所以還是請看下圖:

上圖包含了左旋和右旋的示意圖,這裡以右旋為例進行說明,右旋節點 M 的步驟如下:

  1. 將節點 M 的左孩子引用指向節點 E 的右孩子;
  2. 將節點 E 的右孩子引用指向節點 M,完成旋轉。

上面分析了右旋操作,左旋操作與此類似,大家有興趣自己畫圖試試吧,這裡不再贅述了。旋轉操作本身並不複雜,這裡先分析到這吧。

3.2 插入

紅黑樹的插入過程和二叉查詢樹插入過程基本類似,不同的地方在於,紅黑樹插入新節點後,需要進行調整,以滿足紅黑樹的性質。性質1規定紅黑樹節點的顏色要麼是紅色要麼是黑色,那麼在插入新節點時,這個節點應該是紅色還是黑色呢?答案是紅色,原因也不難理解。如果插入的節點是黑色,那麼這個節點所在路徑比其他路徑多出一個黑色節點,這個調整起來會比較麻煩(參考紅黑樹的刪除操作,就知道為啥多一個或少一個黑色節點時,調整起來這麼麻煩了)。如果插入的節點是紅色,此時所有路徑上的黑色節點數量不變,僅可能會出現兩個連續的紅色節點的情況。這種情況下,通過變色和旋轉進行調整即可,比之前的簡單多了。

接下來,將分析插入紅色節點後紅黑樹的情況。這裡假設要插入的節點為 N,N 的父節點為 P,祖父節點為 G,叔叔節點為 U。插入紅色節點後,會出現5種情況,分別如下:

3.2.1 情況一:插入的新節點 N 是紅黑樹的根節點

這種情況下,我們把節點 N 的顏色由紅色變為黑色,性質2(根是黑色)被滿足。同時 N 被染成黑色後,紅黑樹所有路徑上的黑色節點數量增加一個,性質5(從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點)仍然被滿足。

3.2.2 情況二:N 的父節點是黑色

這種情況下,性質4(每個紅色節點必須有兩個黑色的子節點)和性質5沒有受到影響,不需要調整。

3.2.3 情況三:N 的父節點和叔叔節點 U 都是紅色

N 的父節點是紅色(節點 P 為紅色,其父節點必然為黑色),叔叔節點 U 也是紅色。由於 P 和 N 均為紅色,所有性質4被打破,此時需要進行調整。這種情況下,先將 P 和 U 的顏色染成黑色,再將 G 的顏色染成紅色。此時經過 G 的路徑上的黑色節點數量不變,性質5仍然滿足。但需要注意的是 G 被染成紅色後,可能會和它的父節點形成連續的紅色節點,此時需要遞歸向上調整。

3.2.4 情況四:N 的父節點為紅色,叔叔節點為黑色;節點 N 是 P 的右孩子,且節點 P 是 G 的左孩子

此時先對節點 P 進行左旋,調整 N 與 P 的位置。接下來按照情況五進行處理,以恢復性質4。

這裡需要特別說明一下,上圖中的節點 N 並非是新插入的節點。當 P 為紅色時,P 有兩個孩子節點,且孩子節點均為黑色,這樣從 G 出發到各葉子節點路徑上的黑色節點數量才能保持一致。既然 P 已經有兩個孩子了,所以 N 不是新插入的節點。情況四是由以 N 為根節點的子樹中插入了新節點,經過調整後,導致 N 被變為紅色,進而導致了情況四的出現。考慮下面這種情況(PR 節點就是上圖的 N 節點):

3.2.5 情況五:N 的父節點為紅色,叔叔節點為黑色;N 是 P 的左孩子,且節點 P 是 G 的左孩子

N 的父節點為紅色,叔叔節點為黑色。N 是 P 的左孩子,且節點 P 是 G 的左孩子。此時對 G 進行右旋,調整 P 和 G 的位置,並互換顏色。經過這樣的調整後,性質4被恢復,同時也未破壞性質5。

3.2.6 插入總結

上面五種情況中,情況一和情況二比較簡單,情況三、四、五稍複雜。但如果細心觀察,會發現這三種情況的區別在於叔叔節點的顏色,如果叔叔節點為紅色,直接變色即可。如果叔叔節點為黑色,則需要選選擇,再交換顏色。當把這三種情況的圖畫在一起就區別就比較容易觀察了,如下圖:

3.3 刪除

相較於插入操作,紅黑樹的刪除操作則要更為複雜一些。刪除操作首先要確定待刪除節點有幾個孩子,如果有兩個孩子,不能直接刪除該節點。而是要先找到該節點的前驅(該節點左子樹中最大的節點)或者後繼(該節點右子樹中最小的節點),然後將前驅或者後繼的值複製到要刪除的節點中,最後再將前驅或後繼刪除。由於前驅和後繼至多隻有一個孩子節點,這樣我們就把原來要刪除的節點有兩個孩子的問題轉化為只有一個孩子節點的問題,問題被簡化了一些。我們並不關心最終被刪除的節點是否是我們開始想要刪除的那個節點,只要節點裡的值最終被刪除就行了,至於樹結構如何變化,這個並不重要。

紅黑樹刪除操作的複雜度在於刪除節點的顏色,當刪除的節點是紅色時,直接拿其孩子節點補空位即可。因為刪除紅色節點,性質5(從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點)仍能夠被滿足。當刪除的節點是黑色時,那麼所有經過該節點的路徑上的黑節點數量少了一個,破壞了性質5。如果該節點的孩子為紅色,直接拿孩子節點替換被刪除的節點,並將孩子節點染成黑色,即可恢復性質5。但如果孩子節點為黑色,處理起來就要複雜的多。分為6種情況,下面會展開說明。

在展開說明之前,我們先做一些假設,方便說明。這裡假設最終被刪除的節點為X(至多隻有一個孩子節點),其孩子節點為N,X的兄弟節點為S,S的左節點為 SL,右節點為 SR。接下來討論是建立在節點 X 被刪除,節點 N 替換X的基礎上進行的。這裡說明把被刪除的節點X特地拎出來說一下的原因是防止大家誤以為節點N會被刪除,不然後面就會看不明白。

在上面的基礎上,接下來就可以展開討論了。紅黑樹刪除有6種情況,分別是:

3.3.1 情況一:N 是新的根

在這種情形下,我們就做完了。我們從所有路徑去除了一個黑色節點,而新根是黑色的,所以性質都保持著。

上面是維基百科中關於紅黑樹刪除的情況一說明,由於沒有配圖,看的有點暈。經過思考,我覺得可能會是下面這種情形:

要刪除的節點 X 是根節點,且左右孩子節點均為空節點,此時將節點 X 用空節點替換完成刪除操作。

可能還有其他情形,大家如果知道,煩請告知。

3.3.2 情況二:S 為紅色,其他節點為黑色

這種情況下可以對 N 的父節點進行左旋操作,然後互換 P 與 S 顏色。但這並未結束,經過節點 P 和 N 的路徑刪除前有3個黑色節點(P -> X -> N),現在只剩兩個了(P -> N)。比未經過 N 的路徑少一個黑色節點,性質5仍不滿足,還需要繼續調整。不過此時可以按照情況四、五、六進行調整。

3.3.3 情況三:N 的父節點,兄弟節點 S 和 S 的孩子節點均為黑色

這種情況下可以簡單的把 S 染成紅色,所有經過 S 的路徑比之前少了一個黑色節點,這樣經過 N 的路徑和經過 S 的路徑黑色節點數量一致了。但經過 P 的路徑比不經過 P 的路徑少一個黑色節點,此時需要從情況二開始對 P 進行平衡處理。

3.3.4 情況四:N 的父節點是紅色,S 和 S 孩子為黑色

這種情況比較簡單,我們只需交換 P 和 S 顏色即可。這樣所有通過 N 的路徑上增加了一個黑色節點,所有通過 S 的節點的路徑必然也通過 P 節點,由於 P 與 S 只是互換顏色,並不影響這些路徑。

3.3.5 情況五:S 為黑色,S 的左孩子為紅色,右孩子為黑色

N 的父節點顏色可紅可黑,且 N 是 P 左孩子。這種情況下對 S 進行右旋操作,並互換 S 和 SL 的顏色。此時,所有路徑上的黑色數量仍然相等,N 兄弟節點的由 S 變為了 SL,而 SL 的右孩子變為紅色。接下來我們到情況六繼續分析。

3.3.6 情況六

S 為黑色,S 的右孩子為紅色。N 的父節點顏色可紅可黑,且 N 是其父節點左孩子。這種情況下,我們對 P 進行左旋操作,並互換 P 和 S 的顏色,並將 SR 變為黑色。因為 P 變為黑色,所以經過 N 的路徑多了一個黑色節點,經過 N 的路徑上的黑色節點與刪除前的數量一致。對於不經過 N 的路徑,則有以下兩種情況:

  1. 該路徑經過 N 新的兄弟節點 SL ,那它之前必然經過 S 和 P。而 S 和 P 現在只是交換顏色,對於經過 SL 的路徑不影響;
  2. 該路徑經過 N 新的叔叔節點 SR,那它之前必然經過 P、 S 和 SR,而現在它只經過 S 和 SR。在對 P 進行左旋,並與 S 換色後,經過 SR 的路徑少了一個黑色節點,性質5被打破。另外,由於 S 的顏色可紅可黑,如果 S 是紅色的話,會與 SR 形成連續的紅色節點,打破性質4(每個紅色節點必須有兩個黑色的子節點)。此時僅需將 SR 由紅色變為黑色即可同時恢復性質4和性質5(從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。)。
3.3.7 刪除總結

紅黑樹刪除的情況比較多,大家剛開始看的時候可能會比較暈。可能會產生這樣的疑問,為啥紅黑樹會有這種刪除情況,為啥又會有另一種情況,它們之間有什麼聯絡和區別?和大家一樣,我剛開始看的時候也有這樣的困惑,直到我把所有情況對應的圖形畫在一起時,撥雲見日,一切都明瞭了。此時天空中出現了4個字,原來如此、原來如此、原來如此。所以,請看圖吧:

4.總結

紅黑樹是一種重要的二叉樹,應用廣泛,但在很多資料結構相關的書本中出現的次數並不多。很多書中要麼不說,要麼就一筆帶過,並不會進行詳細的分析,這可能是因為紅黑樹比較複雜的緣故。我在學習紅黑樹的時候也找了很多資料,但總體感覺講的都不太好。尤其是在我學習刪除操作的時候,很多資料是實在人看不下去,看的我很痛苦。直到我看到維基百科上關於紅黑樹的分析時,很是欣喜。這篇文章分析的很有條理,言簡意賅,比很多資料好了太多。本文對紅黑樹的分析也主要參考了維基百科中的紅黑樹分析,並對維基百科中容易讓人產生疑問和誤解的地方進行了說明。同時維基百科中文版紅黑樹文中的圖片較為模糊,這裡我重新進行了繪製。需要說明的是,維基百科中文版無法打開了,文中關於維基百科的連結都是英文版的。另外在給大家推薦一個數據結構視覺化的網站,裡面包含常見資料結構視覺化過程,地址為:t.cn/RZFgryr。

另外,由於紅黑樹本身比較複雜,實現也較為複雜。在寫這篇文章之前,我曾嘗試過用 Java 語言實現紅黑樹的增刪操作,最終只寫出了新增節點操作,刪除沒做出來。而且自己寫的新增邏輯實在太繁瑣,寫的不好看,沒法拿出來 show。所以最後把 Java 中的 TreeMap 增刪相關原始碼拷出來,按照自己的需求把原始碼修改了一下,也勉強算是實現了紅黑樹吧。程式碼放到了 github 上,傳送門 -> RBTree.java。

最後,如果你也在學習紅黑樹,希望這篇文章能夠幫助到你。另外,由於紅黑樹本身比較複雜,加之本人水平有限,難免會出一些錯誤。如果有錯,還望大家指出來,我們共同討論。