- 紅黑樹:紅黑樹是一種二叉平衡樹,二叉查找樹,它牛逼之處就在於它足夠的平衡,可以達到高度至多2lg(n+1),所以在java中的treemap和c++ set, multiset, map, multimap就使用的紅黑樹。
- 紅黑樹的性質:1. 結點分為紅色和黑色兩種 2.根節點是黑色的 3.每個葉子結點(nil)是黑色的(就是空代表了黑色) 4.不存在父子都是紅色的情況(連續兩個紅色) 5.任意孩子到根節點的路徑上的黑色數量都是相等的(important)
- 本文將解釋 以下內容:
1. 紅黑樹的插入(簡單的插入)與調整(調整平衡)。
2. 紅黑樹的刪除(也帶有調整,使其便於調整平衡)與調整(調整平衡)。
插入
插入方式:
先考慮插入什么顏色的結點,這里需要說明顏色只是我們用於保證平衡的一種方法,與數據無關。 因為插入黑色結點除非在根節點,否則一定會導致一個分支上的所有子分支都多了一個黑,如此不滿足性質5(簡稱 路黑同)。所以我們插入紅色。插入方式就是普通的二叉排序樹的插入方式。就這么簡單。
調整:
因為插入的調整相對簡單,我們可以將所有的情況列舉出來然后討論,列舉的依據是:因為插入紅色的結點不會對插入節點的子節點造成任何影響(即符合性質1-5,當我在做任何斷言時,最好自己試試),所以僅需考慮插入結點的祖先的情況
具體而言我們要做的就是討論一段區間,它的所有情況必須滿足調整后對祖先和子孫都而言都仍然平衡,因為原來的兩段都是平衡的,所以只需保證在討論區間中是的任意左右子樹滿足性質,並且不改變原來和祖先和子樹的接口,那么就一定能維持性質不變。
圖中從左到右,分別是紅色,黑色,任意顏色,第二行是對應的附加黑(意思是除了原本結點帶有的顏色還附加一層黑色,用處后面用到的時候再講),
圖中沒有畫出兒子結點不代表它兒子就是nil,如果有葉子結點會標nil的。
下圖為所有情況
情況1:單個結點,沒有父節點。
調整方法:紅色變成黑色,性質2(簡稱 根黑),
情況2: 父節點是黑色,這樣本來就是平衡的,但是由於討論的結點兒子可能是紅色,可以將這種情況轉化成討論它的兒子的情況,它的兒子的父節點是紅色,對應接下來正好要討論的情況3,所以情況2應該算情況3的一種子情況。
情況3:父節點是紅色
分為兩種情況, 一個是父的右孩子,一個是父的左孩子
對於情況3.1.1, 只需將父節點和叔父結點變成黑色,然后祖父結點變成紅色,就可以維持兩個分支的黑色結點一致,並且沒有兩個紅色,可以對照滿足了所有性質,這就算是恢復好了,但僅僅是針對圖中最上方的結點的子樹,若以調整后最上方的紅色結點作為討論對象,討論他的父結點情況,就又可以遞歸的分類討論進行恢復,實際就是算法中的遞歸調用。
對於情況3.1.2 方式類似3.1.1
對於情況3.1.3, 可以先以紅-紅中的父節點進行左旋轉,然后和情況3.2.3一樣了,只需再以它為中心右旋一次,並如圖變換顏色即可,最重要的依據就是分支的黑色要一致。所以調整后整棵樹就恢復好了。因為討論區間分別和子樹以及子樹連接的部分的紅色維持不變,所以不會影響到原本的祖先和子孫的平衡情況。
對於其他情況,可以類似上面的四種情況進行操作,不再一一介紹,為了和教科書上(算法導論)保持一致,並且簡化情況,我們需要將多種情況進行合並。合並的最后結果只剩下三種情況。如圖中棕色的標號, (要記就記以下的結果)
①代表了第一種情況,這里把只有一個紅色結點的情況歸類為了父節點為黑色,實際上也是這么定義的,根節點的父節點為nil,nil為黑色,這樣就進行了統一。
①歸納了情況:插入節點的父和叔節點是紅色時,把他們變黑,再把爺爺變紅,遞歸調用爺爺作為參數繼續算法,
②歸納了情況:插入節點的父是紅色uncle是黑色時,如果自己是父的右結點,先左旋到情況③
③歸納了情況:自己是父的左結點)然后再把fa和uncle變成黑色,grandparent變成紅色再以父親為參數右旋
對應的還有①', ②',③',它和①, ②,③的區別就在於討論的結點是在左子樹還是右子樹,操作方法是對稱的,所以恢復算法一共有六種情況,但是三種也對。
附上代碼:

#插入方法 RB-INSERT(T,z): #z就是插入的結點也是fix時當前討論的結點 y = T.nil x = T.root while x != T.nil y = x if z.key < x.key x = x.left else x = x.right z.p = y if y == T.nil T.root = z else if z.key < y.key y.left = z else y.right = z z.left = T.nil z.right = T.nil z.color = RED RB-INSERT-FIXUP(T,z)

1 RB-INSERT-FIXUP(T,z) 2 while z.p.clolor ==RED 3 if z.p == z.p.p.left 4 y = z.p.p.right 5 if y.color == RED: #case 1 6 z.p.color = BLACK 7 y.color = BLACK 8 z.p.p.color = red 9 z = z.p.p 10 else if z == z.p.right #case 2 11 z = z.p 12 LEFT-ROTATE(T,z) 13 z.p.color=BLACK #case 3 14 z.p.p.color=RED 15 RIGHT-ROTATE(T,z.p,p) 16 else(same as then clause with "right" and "left" exchanged) 17 T.root.color = BLACK
刪除:
刪除情況討論如果按照之前的將所有情況都討論出來,那會很累,並且會繞進去, 所以我們還是根據現有的算法,來討論分類
刪除的方式:根據情況刪除時就做一些調整。
主要的討論分類方式:
將刪除分類為
1.如果刪除的結點只有一個孩子(不考慮nil),又可繼續討論下去,那個孩子是左孩子還是右孩子,
2.如果刪除的結點有兩個孩子,同樣也可詳細討論 ,當然你也可以說還有一種情況,如果刪除的結點一個孩子都沒有,那種情況只用單純的刪除,刪除過程中沒有進行額外的操作,所有刪除操作都要進行刪除步驟,所以這里不進行討論,注意到我們僅討論了刪除節點為根的子樹情況,而沒有討論它的祖先結點,不是因為不影響,而是在刪除過程中僅需要對以上幾種情況分開討論以進行額外的操作,便於刪除恢復。
刪除的具體情況: 刪除過程我們的主要目的是保證通過一些方法讓整棵樹仍然保持平衡(加附加黑的方式),然后在恢復的過程中解決附加黑的問題,以下討論中a為待刪除結點
刪除掉a,將另一個結點作為替換,因為 原來的a結點可能就是黑色的,這樣a的子樹就少了一個黑色,所以將b塗成黑色。我們發現這種情況就不再需要調整了。已經滿足了要求的各種性質,不信可以試試。
如果有兩個孩子的話,並且 a(待刪除結點)的兒子結點的左兒子為nil,就說明a是c最小的后繼,刪除a后,將c移動到a的位置,當然也可以用a的左子樹中最大的數即a的前驅來替換他。除此之外,這種情況下,c必須和a的顏色相同,目的是使得改動最小,但是當a是黑色的時候,由於c,d根據性質必有一黑,當c是黑時,c替換后,c的分支相對於原來,少了一個黑色的結點,這時候就需要多加一重黑色來保證平衡
如果有兩個孩子,並且a的兒子結點的兒子結點不為空,那么,通過找到左兒子的最小值,即dfs一直找最左兒子為nil的d,將它和a進行交換,然后e代替nil,同理還是d代替a的顏色,這時a為黑,且d為黑時就會不平衡,這時候如同情況2也需要給e多加一層黑色。
算法中①的情況因為左為nil和右為nil操作雖然對稱,但是不一樣,所以一般分成四種情況
刪除恢復,
最激動人心的時刻,刪除恢復,不知是誰想的附加黑色這種騷操作,經過了上面的刪除,我們現在只需要考慮恢復的情況只有如何將附加黑情況進行處理,基本思路是只考慮附加黑結點的子樹以外的結點,因為算法試圖將附加黑不斷向樹根傳遞,來達到在某一部消除的方式來恢復。
具體的討論情況分為 附加黑結點的兄弟是1.紅,2.黑(有分為三種情況: 1。兄弟的兒子為全黑 2.左紅右黑 3.左any右紅 )自己畫畫是否討論完全了。 這樣算一共4種子情況如下:
情況1:
這種情況是 附加黑的兄弟是紅色,進行左旋,對應了第二大類的討論。
情況2:
兄弟為黑,並且兄弟兒子全黑,這時候將附加黑向上傳遞,這樣右邊就多了一重黑色,所以將c改成紅色,b的子樹就滿足要求了,然后繼續以b為結點進行遞歸
兄弟為黑並且左紅右黑,通過以c為中心右旋再交換dc顏色就達到了情況4
情況4:
通過以b為中心左旋,這時我們發現左邊和右邊確定的黑分別為2和1,原來是1和1,所以需要給右邊增加一層黑,通過將附加黑給b讓b稱為黑色,同時使得e為黑色,這樣就滿足了左2黑右2黑,但是要保持原來的黑色數為1,所以就將c改成原來b位置應有的任意
- 以上四種情況只考慮了a(帶附加黑的點)是左兒子的情況, 同樣右兒子是對稱,所以和插入一樣,4種基本情況實際是2x4種詳細情況
以下為刪除的偽代碼:

RB-DELETE(T,z): y = z y-origin-color = y.color if z.left == T.nil x = z.right RB-TRANSPLANT(T, z, z.right) else if z.right ==T.nil x = z.left RB-TRANSPLANT(T,z,z.left) else y = TREE-MINIMUN(z,right) y-original-color = y.color x = y.right if y.p == z x.p = y else RB-TRANPLANT(T,y,y.right) y.right = z.right y.right.p = y RB-TRANSPLANT(T,z,y) y.left = z.left y.left.p = y y.color = z.color if y-original-color ==BLACK RB-DELETE-FIXUP(T,x)

RB-DELETE-FIXUP(T,x) while x!=T.root and x.color ==BLACK: if x== x.p.left w = x.p.right if w.color ==RED # case 1 w.color = BLACK x.p.color = RED LEFT-ROTATE(T,x.p) w = x.p.right if w.left.color ==BLACK and w.right.color ==BLACK w.color = RED #case 2 x = x.p else if w.right.color ==BLACK w.left.color = BLACK #case 3 w.color = RED RIGHT-ROTATE(T,w) w = x.p.right w.color = x.p.color #case 4 x.p.color = BLACK w.right.color = BLACK LEFT-ROTATE(T, x.p) x = T.root else (same as then clause with "right" and left exchanged ) #symetric case x.color = BLACK