紅黑樹演算法的實現與剖析
一、紅黑樹的介紹
先來看下演算法導論對R-BTree的介紹:紅黑樹,一種二叉查詢樹,但在每個結點上增加一個儲存位表示結點的顏色,可以是Red或Black。通過對任何一條從根到葉子的路徑上各個結點著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出倆倍,因而是接近平衡的。
前面說了,紅黑樹,是一種二叉查詢樹,既然是二叉查詢樹,那麼它必滿足二叉查詢樹的一般性質。下面,在具體介紹紅黑樹之前,咱們先來了解下二叉查詢樹的一般性質:
1.在一棵二叉查詢樹上,執行查詢、插入、刪除等操作,的時間複雜度為O(lgn)。因為,一棵由n個結點,隨機構造的二叉查詢樹的高度為lgn,所以順理成章,一般操作的執行時間為
//至於n個結點的二叉樹高度為lgn的證明,可參考演算法導論第12章二叉查詢樹第12.4節。
2.但若是一棵具有n個結點的線性鏈,則此些操作最壞情況執行時間為O(n)。
而紅黑樹,能保證在最壞情況下,基本的動態幾何操作的時間均為O(lgn)。
ok,我們知道,紅黑樹上每個結點內含五個域,color,key,left,right,p。如果相應的指標域沒有,則設為NIL。
一般的,紅黑樹,滿足以下性質,即只有滿足以下全部性質的樹,我們才稱之為紅黑樹:
1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點,即空結點(NIL)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。
5)對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。
下圖所示,即是一顆紅黑樹:
此圖忽略了葉子和根部的父結點。
二、樹的旋轉知識
當我們在對紅黑樹進行插入和刪除等操作時,對樹做了修改,那麼可能會違背紅黑樹的性質。
為了保持紅黑樹的性質,我們可以通過對樹進行旋轉,即修改樹種某些結點的顏色及指標結構,以達到對紅黑樹進行
插入、刪除結點等操作時,紅黑樹依然能保持它特有的性質(如上文所述的,五點性質)。
樹的旋轉,分為左旋和右旋,以下藉助圖來做形象的解釋和介紹:
1.左旋
如上圖所示:
當在某個結點pivot上,做左旋操作時,我們假設它的右孩子
左旋以pivot到y之間的鏈為“支軸”進行,它使y成為該孩子樹新的根,而y的左孩子b則成為pivot的右孩子。
來看演算法導論對此操作的演算法實現(以x代替上述的pivot):
LEFT-ROTATE(T, x)
1 y ← right[x] ▹ Set y.
2 right[x] ← left[y] ▹ Turn y's leftsubtree into x's right
subtree.
3 p[left[y]] ← x
4 p[y] ← p[x] ▹ Link x'sparent to y.
5 if p[x] = nil[T]
6 then root[T] ← y
7 else if x = left[p[x]]
8 then left[p[x]] ← y
9 else right[p[x]] ← y
10 left[y] ← x ▹ Put x on y'sleft.
11 p[x] ← y
2.右旋
右旋與左旋差不多,再此不做詳細介紹。
對於樹的旋轉,能保持不變的只有原樹的搜尋性質,而原樹的紅黑性質則不能保持,
在紅黑樹的資料插入和刪除後可利用旋轉和顏色重塗來恢復樹的紅黑性質。
至於有些書如 STL原始碼剖析有對雙旋的描述,其實雙旋只是單旋的兩次應用,並無新的內容,
因此這裡就不再介紹了,而且左右旋也是相互對稱的,只要理解其中一種旋轉就可以了。
三、紅黑樹插入、刪除操作的具體實現
三、I、ok,接下來,咱們來具體瞭解紅黑樹的插入操作。向一棵含有n個結點的紅黑樹插入一個新結點的操作可以在O(lgn)時間內完成。
演算法導論:
RB-INSERT(T, z)
1y ←nil[T]
2x ←root[T]
3whilex ≠ nil[T]
4do y ← x
5if key[z] < key[x]
6then x ← left[x]
7else x ← right[x]
8p[z]← y
9if y= nil[T]
10 then root[T] ← z
11 else if key[z] < key[y]
12then left[y] ← z
13else right[y] ← z
14 left[z] ← nil[T]
15 right[z] ← nil[T]
16 color[z] ← RED
17 RB-INSERT-FIXUP(T, z)
咱們來具體分析下,此段程式碼:
RB-INSERT(T, z),將z插入紅黑樹T
之內。
為保證紅黑性質在插入操作後依然保持,上述程式碼呼叫了一個輔助程式RB-INSERT-FIXUP
來對結點進行重新著色,並旋轉。
14 left[z] ← nil[T]
15 right[z] ← nil[T] //保持正確的樹結構第16行,將z著為紅色,由於將z著為紅色可能會違背某一條紅黑樹的性質,所以,在第17行,呼叫RB-INSERT-FIXUP(T,z)來保持紅黑樹的性質。
RB-INSERT-FIXUP(T,z),如下所示:
1 while color[p[z]] = RED
2do if p[z] = left[p[p[z]]]
3then y ← right[p[p[z]]]
4if color[y] = RED
5then color[p[z]] ←BLACK▹ Case 1
6color[y] ← BLACK▹ Case 1
7color[p[p[z]]] ← RED▹ Case 1
8z ← p[p[z]]▹ Case 1
9else if z = right[p[z]]
10then z ← p[z]▹ Case 2
11LEFT-ROTATE(T,z)▹ Case 2
12color[p[z]] ←BLACK▹ Case 3
13color[p[p[z]]] ←RED▹ Case 3
14RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3
15else (same as then clause
with "right"and "left" exchanged)
16 color[root[T]] ← BLACK
ok,參考一網友的言論,用自己的語言,再來具體解剖下上述倆段程式碼。為了保證闡述清晰,我再寫下紅黑樹的5個性質:
1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點,即空結點(NIL)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。
5)對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。
在對紅黑樹進行插入操作時,我們一般總是插入紅色的結點,因為這樣可以在插入過程中儘量避免對樹的調整。那麼,我們插入一個結點後,可能會使原樹的哪些性質改變列?
由於,我們是按照二叉樹的方式進行插入,因此元素的搜尋性質不會改變。
如果插入的結點是根結點,性質2會被破壞,如果插入結點的父結點是紅色,則會破壞性質4。因此,總而言之,插入一個紅色結點只會破壞性質2或性質4。我們的回覆策略很簡單,其一、把出現違背紅黑樹性質的結點向上移,如果能移到根結點,那麼很容易就能通過直接修改根結點來恢復紅黑樹的性質。直接通過修改根結點來恢復紅黑樹應滿足的性質。其二、窮舉所有的可能性,之後把能歸於同一類方法處理的歸為同一類,不能直接處理的化歸到下面的幾種情況,
//注:以下情況3、4、5與上述演算法導論上的程式碼RB-INSERT-FIXUP(T, z),相對應:
情況1:插入的是根結點。原樹是空樹,此情況只會違反性質2。對策:直接把此結點塗為黑色。情況2:插入的結點的父結點是黑色。此不會違反性質2和性質4,紅黑樹沒有被破壞。對策:什麼也不做。情況3:當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色。此時父結點的父結點一定存在,否則插入前就已不是紅黑樹。與此同時,又分為父結點是祖父結點的左子還是右子,對於對稱性,我們只要解開一個方向就可以了。
在此,我們只考慮父結點為祖父左子的情況。同時,還可以分為當前結點是其父結點的左子還是右子,但是處理方式是一樣的。我們將此歸為同一類。對策:將當前節點的父節點和叔叔節點塗黑,祖父結點塗紅,把當前結點指向祖父節點,從新的當前節點重新開始演算法。
針對情況3,變化前(圖片來源:saturnman)[插入4節點]:
變化後:
情況4:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的右子
對策:當前節點的父節點做為新的當前節點,以新當前節點為支點左旋。
如下圖所示,變化前[插入7節點]:
變化後:
情況5:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的左子
解法:父節點變為黑色,祖父節點變為紅色,在祖父節點為支點右旋
如下圖所示[插入2節點]
變化後:
三、II、ok,接下來,咱們最後來了解,紅黑樹的刪除操作:
演算法導論一書,給的演算法實現:
RB-DELETE(T,z) 單純刪除結點的總操作
1 if left[z] = nil[T] or right[z] = nil[T]
2then y ← z
3else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ nil[T]
5then x ← left[y]
6else x ← right[y]
7 p[x] ← p[y]
8 if p[y] = nil[T]
9then root[T] ← x
10 else if y = left[p[y]]
11 then left[p[y]] ← x
12else right[p[y]] ← x
13 if y 3≠ z
14 then key[z] ← key[y]
15copy y's satellite data into z
16 if color[y] = BLACK
17 then RB-DELETE-FIXUP(T, x)
18 return y
RB-DELETE-FIXUP(T,x) 恢復與保持紅黑性質的工作
1 while x ≠ root[T] and color[x]= BLACK
2doif x = left[p[x]]
3then w ← right[p[x]]
4if color[w] = RED
5then color[w] ← BLACK▹Case 1
6color[p[x]] ← RED▹Case 1
7LEFT-ROTATE(T,p[x])▹Case 1
8w ← right[p[x]]▹Case 1
9if color[left[w]] = BLACK andcolor[right[w]] = BLACK
10then color[w] ← RED▹Case 2
11x p[x]▹Case 2
12else if color[right[w]] =BLACK
13then color[left[w]]← BLACK▹Case 3
14color[w] ←RED▹Case 3
15RIGHT-ROTATE(T,w)▹Case 3
16w ←right[p[x]]▹Case 3
17color[w] ←color[p[x]]▹Case 4
18color[p[x]] ←BLACK▹Case 4
19color[right[w]] ←BLACK▹Case 4
20LEFT-ROTATE(T,p[x])▹Case 4
21x ← root[T]▹Case 4
22else (same as then clause with"right" and "left" exchanged)
23 color[x] ← BLACK
為了保證以下的介紹與闡述清晰,我第三次重寫下紅黑樹的5個性質
1)每個結點要麼是紅的,要麼是黑的。
2)根結點是黑的。
3)每個葉結點,即空結點(NIL)是黑的。
4)如果一個結點是紅的,那麼它的倆個兒子都是黑的。
5)對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。
(相信,重述了3次,你應該有深刻記憶了。:D)
saturnman:紅黑樹刪除的幾種情況:
-------------------------------------------------
博主提醒:以下所有的操作,是針對紅黑樹已經刪除結點之後,為了恢復和保持紅黑樹原有的5點性質,所做的恢復工作。
前面,我已經說了,因為插入、或刪除結點後,可能會違背、或破壞紅黑樹的原有的性質,所以為了使插入、或刪除結點後的樹依然維持為一棵新的紅黑樹,那就要做倆方面的工作:
1、部分結點顏色,重新著色
2、調整部分指標的指向,即左旋、右旋。
而下面所有的文字,則是針對紅黑樹刪除結點後,所做的修復紅黑樹性質的工作。
二零一一年一月七日更新。
------------------------------------------------------------
(注:以下的情況3、4、5、6,與上述演算法導論之程式碼RB-DELETE-FIXUP(T,x)
恢復與保持中case1,case2,case3,case4相對應。)
情況1:當前節點是紅色解法,直接把當前節點染成黑色,結束。此時紅黑樹性質全部恢復。情況2:當前節點是黑色且是根節點解法:什麼都不做,結束
情況3:當前節點是黑色,且兄弟節點為紅色(此時父節點和兄弟節點的子節點分為黑)。解法:把父節點染成紅色,把兄弟結點染成黑色,之後重新進入演算法(我們只討論當前節點是其父節點左孩子時的情況)。然後,針對父節點做一次左旋。此變換後原紅黑樹性質5不變,而把問題轉化為兄弟節點為黑色的情況。
3.變化前:
3.變化後:
情況4:當前節點是黑色,且兄弟是黑色,且兄弟節點的兩個子節點全為黑色。解法:把當前節點和兄弟節點中抽取一重黑色追加到父節點上,把父節點當成新的當前節點,重新進入演算法。(此變換後性質5不變)