樹結構學習二:平衡二叉樹、AVL樹、紅黑樹

前面說到二叉樹在極端情況下會退化成鏈表,那如何解決這個問題呢?

答案是:樹的平衡。我們通過樹的平衡,使得左右子樹的深度保持在較小範圍內,從而保證二叉樹的查詢效率。 這就是平衡二叉樹的核心思想。 這種能平衡左右子樹的二叉樹,我們稱之爲平衡二叉樹。

官方對於平衡樹的定義是:任意節點的子樹的高度差都小於等於 1。 常見的符合平衡樹的有:2-3 樹、B 樹、AVL 樹等。紅黑樹是一種特殊的自平衡樹,其子樹的高度差並不一定小於等於 1。AVL 樹雖然查詢效率高,但是插入、刪除效率低,需要不斷旋轉以保持平衡。而紅黑樹通過犧牲一些查詢效率,提高了插入、刪除的效率。

AVL樹

AVL 樹是最早發明的自平衡二叉查找樹。 在 AVL 樹中任何節點的兩個子樹的高度最大差別爲 1,所以它也被稱爲高度平衡樹。 增加和刪除可能需要通過一次或多次樹旋轉來重新平衡這個樹。AVL 樹得名於它的發明者 G. M. Adelson-Velsky 和 E. M. Landis,他們在 1962 年的論文《An algorithm for the organization of information》中發表了它。

AVL 樹本質上還是一棵二叉搜索樹,但是其比二叉搜索樹還多了平衡功能。它的特點是:

  1. 本身首先是一棵二叉搜索樹。
  2. 帶有平衡條件:每個結點的左右子樹的高度之差的絕對值(平衡因子)最多爲 1。

也就是說,AVL 樹本質上是帶了平衡功能的二叉搜索樹。

AVL 樹的旋轉操作,本質上和紅黑樹的類似,這裏就不細講。我們在下面講解紅黑樹的時候再展開說。

紅黑樹

紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構。紅黑樹是在 1972 年由 Rudolf Bayer 發明的,當時被稱爲平衡二叉 B 樹(symmetric binary B-trees)。後來,在 1978 年被 Leo J. Guibas 和 Robert Sedgewick 修改爲如今的「紅黑樹」。

紅黑樹是一種特化的 AVL 樹(平衡二叉樹),都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。

它雖然是複雜的,但它的最壞情況運行時間也是非常良好的,並且在實踐中是高效的: 它可以在 O (log N) 時間內做查找、插入和刪除,這裏的 N 是樹中元素的數目。

對於紅黑樹來說,其可能有如下一些特點:

  1. 若一棵二叉查找樹是紅黑樹,則它的任一子樹必爲紅黑樹。
  2. 紅黑樹是一種平衡二叉查找樹的變體,它的左右子樹高差有可能大於 1。
  3. 與 AVL 樹相比,其通過犧牲查詢效率來提升插入、刪除效率。

紅黑樹是在二叉查找樹的基礎上演化進來的,除了二叉查找樹的要求之外,紅黑樹還具有如下五個強制要求(屬性):

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

上面這 5 個性質使得紅黑樹有一個關鍵的性質:從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。 爲何會有這樣一個結果,其實從性質中我們就可以大概猜出一二。

性質 4 與性質 5 是紅黑樹關鍵的核心性質。 性質 4 表明一個節點到根節點的最短可能路徑都是黑色節點,最長可能路徑有交替紅色和黑色節點。根據性質 5 所有最長的路徑都有相同數目的黑色結點,這就表明了沒有路徑能多於任何其他路徑的兩倍長。

當我們在對紅黑樹進行插入和刪除等操作時,對樹做了修改,那麼可能會違背紅黑樹的性質。爲了保持紅黑樹的性質,我們可以對相關結點做一系列的調整,通過對樹進行旋轉(例如左旋和右旋操作),即修改樹中某些結點的顏色及指針結構,以達到對紅黑樹進行插入、刪除結點等操作時,紅黑樹依然能保持它特有的五個性質。

紅黑樹操作

本部分內容來自田小波的技術博客,地址:紅黑樹詳細分析 | 田小波的技術博客

紅黑樹的基本操作和其他樹形結構一樣,一般都包括查找、插入、刪除等操作。前面說到,紅黑樹是一種自平衡的二叉查找樹,既然是二叉查找樹的一種,那麼查找過程和二叉查找樹一樣,比較簡單,這裏不再贅述。相對於查找操作,紅黑樹的插入和刪除操作就要複雜的多。

尤其是刪除操作,要處理的情況比較多,不過大家如果靜下心來去看,會發現其實也沒想的那麼難。好了,廢話就說到這,接下來步入正題吧。

旋轉操作

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

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

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

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

插入操作

紅黑樹的插入過程和二叉查找樹插入過程基本類似,不同的地方在於,紅黑樹插入新節點後,需要進行調整,以滿足紅黑樹的性質。性質 1 規定紅黑樹節點的顏色要麼是紅色要麼是黑色,那麼在插入新節點時,這個節點應該是紅色還是黑色呢?

答案是紅色,原因也不難理解。如果插入的節點是黑色,那麼這個節點所在路徑比其他路徑多出一個黑色節點,這個調整起來會比較麻煩(參考紅黑樹的刪除操作,就知道爲啥多一個或少一個黑色節點時,調整起來這麼麻煩了)。

如果插入的節點是紅色,此時所有路徑上的黑色節點數量不變,僅可能會出現兩個連續的紅色節點的情況。這種情況下,通過變色和旋轉進行調整即可,比之前的簡單多了。

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

情況一

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

情況二

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

情況三

N 的父節點是紅色(節點 P 爲紅色,其父節點必然爲黑色),叔叔節點 U 也是紅色。由於 P 和 N 均爲紅色,所有性質 4 被打破,此時需要進行調整。這種情況下,先將 P 和 U 的顏色染成黑色,再將 G 的顏色染成紅色。

此時經過 G 的路徑上的黑色節點數量不變,性質 5 仍然滿足。但需要注意的是 G 被染成紅色後,可能會和它的父節點形成連續的紅色節點,此時需要遞歸向上調整。

情況四

N 的父節點爲紅色,叔叔節點爲黑色。節點 N 是 P 的右孩子,且節點 P 是 G 的左孩子。此時先對節點 P 進行左旋,調整 N 與 P 的位置。接下來按照情況五進行處理,以恢復性質 4。

情況五

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

插入總結

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

刪除操作

相較於插入操作,紅黑樹的刪除操作則要更爲複雜一些。刪除操作首先要確定待刪除節點有幾個孩子,如果有兩個孩子,不能直接刪除該節點。而是要先找到該節點的前驅(該節點左子樹中最大的節點)或者後繼(該節點右子樹中最小的節點),然後將前驅或者後繼的值複製到要刪除的節點中,最後再將前驅或後繼刪除。

由於前驅和後繼至多隻有一個孩子節點,這樣我們就把原來要刪除的節點有兩個孩子的問題轉化爲只有一個孩子節點的問題,問題被簡化了一些。我們並不關心最終被刪除的節點是否是我們開始想要刪除的那個節點,只要節點裏的值最終被刪除就行了,至於樹結構如何變化,這個並不重要。

紅黑樹刪除操作的複雜度在於刪除節點的顏色,當刪除的節點是紅色時,直接拿其孩子節點補空位即可。因爲刪除紅色節點,性質 5(從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點)仍能夠被滿足。當刪除的節點是黑色時,那麼所有經過該節點的路徑上的黑節點數量少了一個,破壞了性質 5。

如果該節點的孩子爲紅色,直接拿孩子節點替換被刪除的節點,並將孩子節點染成黑色,即可恢復性質 5。但如果孩子節點爲黑色,處理起來就要複雜的多。分爲 6 種情況,下面會展開說明。

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

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

情況一

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

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

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

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

情況二

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

情況三

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

情況四

N 的父節點爲紅色,叔叔節點爲黑色。節點 N 是 P 的右孩子,且節點 P 是 G 的左孩子。此時先對節點 P 進行左旋,調整 N 與 P 的位置。接下來按照情況五進行處理,以恢復性質 4。

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

如上圖,插入節點 N 並按情況三處理。此時 PR 被染成了紅色,與 P 節點形成了連續的紅色節點,這個時候就需按情況四再次進行調整。

情況五

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

情況六

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(從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點)。

刪除總結

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

總結

通過平衡二叉樹的旋轉調整,我們解決了二叉搜索樹退化成鏈表的問題。平衡二叉樹是任意節點的子樹的高度差都小於等於 1 的二叉搜索樹,其最早最典型的應用就是 AVL 樹。

雖然 AVL 解決了平衡的問題,但 AVL 樹存在插入、刪除慢的特點。爲了解決該問題,紅黑樹應運而生。其通過犧牲部分查詢效率,提升了插入、刪除效率,使其在最壞情況下也能實現 O (log N) 的時間複雜度。接着我們深入介紹了紅黑樹的旋轉操縱,揭示了紅黑樹是如何實現平衡操作的。

說到這裏,紅黑樹應該說已經是查找、搜索的極限了。但事實上紅黑樹也有其侷限性,即在空間存儲方面的侷限性。假設我們現在也在 10 億個數字裏面查找某個數,那麼我們應該怎麼辦?

一種最簡單的辦法就是直接把 10 億個數加載到內存中,然後在內存中去比較他們的大小。這看着沒問題,但實際上內存能裝得下 10 億個數字麼?如果裝不下,那麼我們應該怎麼解決這個問題?

上面這個例子的典型應用場景就是我們的數據庫索引。對於數據庫來說,一個表幾十億的數據是非常正常的。在這種情況下,B 樹應運而生,其就是用來解決海量數據的搜索難題的。

下篇文章,我們就來講講 B 樹是如何解決海量數據的搜索問題。

參考資料

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章