紅黑樹是一個比較複雜的資料結構,相信很多人也只知其名而不知其意,因為理解它的原理確實需要花費一定的功夫。之所以寫這篇文章,也是為了更好的理解 Java 中 TreeMap 的原始碼。
寫之前,搜了下網上的文章,說實話,看完有點懵,大部分一上來就給你它的五大性質,然後就是一頓插入、刪除、旋轉操作,就完事了,理解起來相當吃力。
本文將結合 2-3-4 樹,循序漸進地介紹紅黑樹的由來和原理,相信看完之後,你對它會有更清晰的認識。此外,這裡描述的是普通紅黑樹,而不是它的變體左傾紅黑樹(LLRB),這一點需要注意。
紅黑樹的引入
二叉查詢樹
紅黑樹的由來要從二叉查詢樹說起。二叉查詢樹是一顆二叉樹,它每個結點的值都大於其左子樹的任意結點而小於右子樹的任意結點,它結合了連結串列插入的靈活性和有序陣列查詢的高效性(二分查詢)。
對於使用二叉查詢樹的演算法,它的執行時間取決於樹的形狀,而樹的形狀又取決於結點插入的先後順序。如上圖所示,最好情況下,N 個結點的樹是完全平衡的,每條空連結到根結點的距離都為 ~lgN;而在最壞的情況下,搜尋路徑上可能有 N 個結點,退化成了連結串列。
所以,為了保證執行時間始終在對數級別,在動態構建二叉查詢樹時,希望保持其平衡性,也就是降低樹的高度,使其儘可能為 ~lgN,這樣就能保證所有的查詢都能在 ~lgN 次比較內結束,就像二分查詢那樣,這樣的樹被稱為平衡二叉查詢樹。
AVL 樹
第一個自平衡二叉查詢樹就是AVL 樹,它規定,每個結點的左右子樹的高度之差不超過 1。在插入或刪除結點,打破平衡後,就會通過一次或多次樹旋轉來重新平衡。
AVL 樹是嚴格平衡的,適用於查詢密集型應用程式,因為在頻繁插入或刪除結點的場景下,它花費在樹旋轉的代價太高。
而紅黑樹就是一種折中方案,它不追求完美平衡,只求部分達到平衡,從而降低在調整時樹旋轉次數。
2-3-4 樹
說到紅黑樹,就不得不提 2-3-4 樹,因為,紅黑樹可以說就是它的一種特殊實現,對它有所瞭解,非常有助於理解紅黑樹。
保持平衡,無非是為了降低樹的高度,如果把二叉查詢樹一般化,允許一個結點儲存多個值,變成多叉樹,也可認為是降低了高度。
確切地說,標準二叉查詢樹中的結點稱為2-結點(一個值兩個子結點),現在引入3-結點(兩個值三個子結點)和4-結點(三個值四個子結點),這樣就能得到一顆 2-3-4 樹(也稱為 2-4 樹)。
2-3-4 樹是 4 階 B 樹,所有資料按排序順序儲存,所有葉子結點都在相同的深度。對於大多數程式語言,直接實現 2-3-4 樹比較困難,而紅黑樹的實現相對要簡單容易,這也是紅黑樹應用廣泛的一部分原因。
紅黑樹是二叉樹,所有的結點都是2-結點,所以為了能夠表示3-結點和4-結點,為結點引入了顏色屬性:
- 黑色,表示普通結點
- 紅色,表示可與父結點合併看作多值結點
如上圖所示,如果把紅黑樹的紅色結點和其父結點放平,它的結構就和左邊的 2-3-4 樹一樣。
紅黑樹
現在,來看下紅黑樹的性質:
- 每個結點都是紅色或黑色的
- 根結點是黑色的(是紅色最終也會轉黑色)
- 所有葉子結點都是黑色的,這裡的葉子結點指的是空結點,常用 NIL 表示
- 如果結點為紅色,則其子結點均為黑色(紅色表示可與父結點合併,子結點湊什麼熱鬧)
- 從給定結點到其任何後代 NIL 結點的每條路徑都包含相同數量的黑色節點(轉成 2-4 樹,所有葉子節點均在最底層)
這些性質不必去背,就算記住後也絕對會忘,應該結合著 2-3-4 樹理解性記憶。
另外,紅黑樹中的旋轉和顏色翻轉,就相當於 2-3-4 樹中的拆分和合併,並且 2-3-4 樹結點的拆分和合並,理解起來相當簡單。對比分析和理解紅黑樹的操作,絕對讓你眼前一亮。
樹旋轉
在分析插入和刪除之前,先了解下什麼是樹旋轉。樹旋轉是二叉樹中調整子樹的一種操作,常用於調整樹的區域性平衡性,它包含兩種方式,左旋轉和右旋轉。
其實旋轉操作很容易理解:左旋轉就是將用兩個結點中的較小者作為根結點變為將較大者作為根結點,右旋轉剛好於此相反,如上圖所示:
- 右旋轉,就是將較小者 L 作為根結點,然後調整 L 和 P 的子樹
- 左旋轉,就是將較大者 P 作為根結點,然後調整 P 和 L 的子樹
紅黑樹的旋轉其實就是為了確保和其結構相同的 2-3-4 樹的一一對應關係,同時保證紅黑樹的有序性和平衡性。
插入
接下來,就結合 2-4 樹分析結點的插入,首先 2-4 樹的插入邏輯是這樣的:
- 如果是 2-結點,直接插入變成 3-結點
- 如果是 3-結點,直接插入變成 4-結點
- 如果是 4-結點,首先進行分裂,變成 2-結點,再插入
2-4 樹插入的都是葉子結點,紅黑樹插入的結點都是紅色的,因為在 2-4 樹中,待插入結點都認為可以插入到一個多值結點中。
這裡假設待插入結點為 N,P 是 N 的父結點,G 是 N 的祖父結點,U 是 N 的叔叔結點(即父結點的兄弟結點),那麼紅黑樹有以下幾種插入情況:
- N 是根結點,即紅黑樹的第一個結點
- N 的父結點(P)為黑色
- P 是紅色的(不是根結點),它的兄弟結點 U 也是紅色的
- P 為紅色,而 U 為黑色
情況 1,2,3
這三種情況比較簡單,就放在一起說明了,它們都不涉及旋轉,只涉及顏色翻轉,換句話說就是隻是結點合併沒有拆分。
情況 1 和 2,不影響紅黑樹的性質,不會打破平衡,直接插入即可:
- 對於 2-4 樹來說,空樹插入就是一個 2-結點->3-結點->4-結點轉換的過程
- 紅黑樹就是建立一個根結點為黑色的標準 2-結點
情況 3,P 為紅色(不是根結點),U 也是紅色,兩個樹插入情況如下:
- 在 2-4 樹中,就意味著這是個 4-結點,它首先拆分成 2-結點,然後再進行插入
- 對於紅黑樹,它相當於已經拆分,直接變色即:P 和 U 變成黑色,G 變成紅色,若 G 是根結點,直接變黑,否則遞迴向上檢查是否造成不平衡
以 [7, 5, 9, 3] 輸入序列為例,兩個樹構建過程如下:
情況 4
情況 4,P 為紅色,而 U 為黑色,此時,在 2-4 樹看來這個結點就是一個 3-結點,直接插入變成 4-結點;而對於紅黑樹,它為了和這個 2-4 樹結構保持一致,會根據不同的情況做旋轉,分別有以下四種可能:
- P 是 G 的左孩子,若 N 是 P 的左孩子,那麼將祖父結點 G 右旋轉 即可
- P 是 G 的左孩子,若 N 是 P 的右孩子,那麼 P 先左旋轉,然後再將祖父結點 G 右旋轉
相反的:
- P 是 G 的右孩子,若 N 是 P 的右孩子,那麼將祖父結點 G 左旋轉 即可
- P 是 G 的右孩子,若 N 是 P 的左孩子,那麼 P 先右旋轉,然後再將祖父結點 G 左旋轉
以 [7, 5, 9, 3, 4] 輸入序列為例,也就是在上圖的基礎上,插入 4,演示 P 為左,N 為右,樹的旋轉過程:
其他情況,左右互換即可,可自行嘗試分析。這裡給出最開始提供的紅黑樹和 2-3-4 樹它們的動態構建過程,輸入序列為 [7, 5, 9, 3, 4, 8, 10, 11, 12, 13, 14, 15],首先是 2-3-4 樹的構建:
紅黑樹構建時會有一次根結點調整,可注意一下:
刪除
二叉查詢樹的結點無非是有兩個子結點,有一個子結點和葉子結點三種,其中有兩個子結點的 M 結點的刪除邏輯是:
- 首先尋找 M 結點左子樹最大或右子樹最小的結點 X
- 然後把 X 結點的值複製到 M 結點
- 最後刪除 X 結點,而這個結點要麼是葉子結點,要麼就只有一個孩子
所以,刪除任一結點的問題就簡化成了:刪除一個最多隻有一個孩子的結點的情況,並且所有的刪除操作都在葉子結點完成,只不過刪除的結點不再是一開始想刪除的結點,但結點的值最終是刪除了,而樹結構的變化與簡化問題相比,並不重要。
在分析紅黑樹的刪除之前,簡單來看下 2-3-4 樹的刪除情況。
2-3-4 樹結點刪除
它類似二叉查詢樹的刪除,實際的刪除操作也是在葉子結點完成,只不過在刪除的過程中涉及到結點的合併,主要有 3 種不同的情況:
- 如果元素 K 是內部結點,並且在一個至少有 2 個 key 的多值葉子結點內部,則只需從結點中刪除 K
- 如果元素 K 是內部結點,且有左孩子和右孩子,那麼:
2.1 如果左孩子至少有 2 個 key,那麼找一個最大值替換 K,然後刪除這個最大值
2.2 如果右孩子至少有 2 個 key,那麼找一個最小值替換 K,然後刪除這個最小值
2.3 如果兩個孩子都只有 1 個 key,那麼將 K 下沉,與其子女合併,形成一個至少有 2 個 key 的結點,最後再刪除 K - 如果元素 K 不是內部結點,所在結點只有它 1 個 key,那麼根據以下情況,最終會轉成情況1或情況2:
3.1 如果它的兄弟結點至少有 2 個 key,那麼選擇一個推到父節點中,再把舊的父節點下沉和 K 合併
3.2 如果它的兄弟結點也只有 1 個 key,那麼將父結點下沉,與其子女合併,再刪除 K,所以,此時需要父結點至少有 2 個 key,如果沒有那麼在父結點上遞迴按情況 3 處理
上面這些情況,有一個前提就是,在遍歷查詢待刪除結點時,必須保證路過的結點都至少有 2 個 key,不是的話就需要合併結點。這點比較難理解,在插入時,會把遍歷過程中遇到的4-結點 進行拆分,相對的,在刪除時,就要保證遍歷的結點至少有 2 個 key,也就相當於把之前拆分的進行了合併。
以下圖示演示了上述的每種可能的刪除情況:
簡單來說,理解 2-3-4 樹刪除的重點就是:
- 如果刪除的結點是多值結點,直接刪除即可
- 否則從兄弟結點獲取一個多餘的結點填補空缺
- 再否則就從父結點獲取一個結點填補空缺,如果父結點沒有多餘結點,將問題遞迴到父結點處理。
紅黑樹的刪除
紅黑樹的刪除也同樣類似二叉查詢樹,不過要考慮平衡,也就是結點顏色問題,要麻煩一點。
首先宣告一點,接下來說的紅黑樹葉子結點和二叉查詢樹葉子結點相同,如果要強調紅黑樹結點是空的葉子結點 NIL 會特殊說明,畫圖會使用黑色方框表示。
假設待刪除結點為 M,如果有非葉子結點,稱為 C,那麼有兩種比較簡單的刪除情況:
- M 為紅色結點,那麼它必是葉子結點,直接刪除即可,因為如果它有一個黑色的非葉子結點,那麼就違反了性質5,通過 M 向左或向右的路徑黑色結點不等
- M 是黑色而 C 是紅色,只需要讓 C 替換到 M 的位置,並變成黑色即可,或者說交換 C 和 M 的值,並刪除 C(就是第一個簡單的情況)。
注意:M 有且僅有一個非葉子的左或右孩子結點,相當於 2-3-4 樹刪除的情況 1。
這兩個情況,本質都是刪除了一個紅色結點,不影響整體平衡。以 [7, 5, 9, 3, 4] 輸入序列構建的紅黑樹為例,演示以上兩種比較簡單的情況:
刪除比較複雜的是 M 和 C 都是黑色的情況,此時 M 肯定是葉子節點,而 C 肯定是 NIL 結點,如果不是這樣的情況將違反性質5。
一個黑色結點被刪除會打破平衡,需要找一個結點填補這個空缺,假設待刪除結點為 M,刪除後它的位置上就變成了 NIL 結點,為了方便描述,這個結點記為 N,P 表示 N 的父結點,S 表示 N 兄弟結點,S 如果存在左右孩子,分別使用 SL 和 SR 表示,那麼刪除就有以下幾種情況:
情況 1 - N 是根結點
刪除後,N 變成了根結點,也就是說刪除前只有 M 這一個結點,直接刪除即可。
情況 2 - P 黑 S 紅
S 是紅色,那麼它必有兩個孩子結點,且都為黑色,而且 P 也肯定是黑色。此時,交換 P 和 S 的顏色,然後對 P 左旋轉,如下:
現在,結點 N 的父結點變成了紅色,兄弟結點變成了 SL,此時就可以按照情況 4、5、6繼續處理。
情況 3 - P 黑 S 黑
P 是黑色,S 也是黑色,並且 S 也沒有非空的孩子結點。此時,直接將 S 變成紅色,那麼經過 S 的路徑也就少了一個黑色結點,整體上就導致經過 P 的路徑比原來少了一個黑色結點,把不平衡狀態從結點 N 轉移到了結點 P,可以把 P 按 情況1 處理,直到遇到根結點,以此形成遞迴:
情況 4 - P 紅 S 黑
P 是紅色,S 是黑色,並且 S 也沒有非空的孩子結點。此時,只要交換 P 和 S 的顏色,正好填補了少一個黑色結點的空缺,也就是恢復了平衡的狀態:
情況 5 - P 任意 S 黑 SL 紅
P 任意顏色,S 黑色,S 的左孩子紅色,(S 有右孩子也是紅色)。此時,對 S 右旋轉,並交換 S 和 SL 的顏色:
其實就是把這種情況,轉成了 情況 6 進行處理。
情況 6 - P 任意 S 黑,SR 紅
P 任意顏色,S 黑色,S 的右孩子紅色,(S 有左孩子也是紅色)。此時,對 P 左旋轉,交換 P 和 S 的顏色,並將 SR 變成黑色:
此時恢復平衡的狀態,無論 P 之前是什麼顏色,N 都比之前多了一個黑色父結點。假設 P 原先是紅色的,現在變成了黑色;假設原先是黑色的,現在 P 又多了一個黑色的父結點 S,所以,無論怎樣,經過結點 N 路徑增加了一個黑色結點。
以上 6 種情況,結點 N 都是左孩子,如果是右孩子,只需把左右對調即可。類比 2-3-4 樹的刪除,理解黑色結點刪除後的關鍵就是:
- 如果兒子結點中有紅色的則從兒子結點中選一結點填補被刪除後的空缺
- 否則,從兄弟結點中選擇一個結點填補空缺
- 再否則,從父結點中選擇一個結點填補空缺,將問題遞迴到父結點處理
最後來看下 2-3-4 樹和紅黑樹動態刪除的過程,輸入序列為 [7, 5, 9, 3, 4, 8, 10, 11, 12, 13, 14, 15, 16],刪除順序是 [16, 13, 11, 7, 15, 14, 8, 4, 9, 10, 5, 3, 12],首先是 2-3-4 樹的動態刪除過程:
紅黑樹動態刪除的過程:
小結
紅黑樹確實比較複雜,單純的分析性質和旋轉,意義不大,而 2-3-4 樹就不一樣了,它的插入和刪除簡單多了,而紅黑樹的旋轉和變色最終也是為了和同構的 2-3-4 樹保持一致,本文就是相互結合分析,互相印證,相信會相對容易理解一點。
動圖來自網站:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
它支援單步除錯,有興趣可以試一下。