演算法導論 第十三章:紅黑樹 筆記(紅黑樹的性質、旋轉、插入、刪除)
紅黑樹(red-black tree) 是許多“平衡的”查詢樹中的一種,它能保證在最壞情況下,基本的動態集合操作的時間為O(lgn) 。
紅黑樹的性質:
紅黑樹是一種二叉查詢樹,但在每個結點上增加一個儲存位表示結點的顏色,可以是RED或BLACK 。通過對任何一條從根到葉子的路徑上各個結點著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡的。
樹中每個結點包含五個域: color, key, left, right和p。如果某結點沒有一個子結點或父結點,則該結點相應的指標(p)域包含值NIL。我們將把這些NIL視為指向二叉查詢樹的外結點(葉子)的指標,而把帶關鍵字的結點視為樹的內結點。
紅黑樹有以下五點性質:
1、每個節點或是紅色的,或是黑色的;
2、根節點是黑色的;
3、每個葉節點(NIL)是黑色的;
4、如果一個節點是紅色的,則其兩個子節點都是黑色的;
5、對於每個節點,從該節點到其所有後代葉節點的簡單路徑上,均包含相同數目的黑色節點。
當滿足以上5個性質的紅黑樹,就能保證沒有一條路徑會比其他路徑長2倍以上,因而可看作近似平衡。
黑高度:
從某個節點x出發到大一個葉節點的任意一條路徑上,黑色節點的個數稱為該節點的黑高度,用bh(x)表示。
以任一結點x為根的子樹至少包含2的bh(x)次方−1個內部結點。
紅黑樹是一種好的二叉查詢樹,對一棵有n個內節點的紅黑樹的高度至多為2lg(n+1),STL中的set和map就是用紅黑樹實現的。紅黑樹的動態集合操作SEARCH, MINIMUM, MAXIMUM, SUCCESSOR, PREDECESSOR與二叉查詢樹的對應操作的實現一樣,且它們的時間複雜度都是O(lgn)。
為了便於處理紅黑樹程式碼中的邊界條件,我們採用一個哨兵來代表NIL。對一棵紅黑樹T 來說,哨兵nil[T] 是一個與樹內普通結點有相同域的物件。它的color 域為BLACK, 而它的其他域p, left, right 和key可以設定成任意允許的值。如下圖所示,所有指向NIL的指標都被替換成指向哨兵nil[T]的指標。
使用哨兵後,就可以將結點x的NIL孩子視為一個其父結點為x的普通結點。雖然我們可以在樹內的每一個NIL 上新增一個不同的哨兵結點,來讓每個NIL的父結點都有這樣的定義,但是這種做法會浪費空間。
我們的做法是採用一個哨兵nil[T]來代表所有的NIL——所有的葉子以及根部的父結點。哨兵的域p, left, right 以及key 的取值如何並不重要,為了方便起見,也可以在程式中設定它們。
在本章中,我們使用忽略了所有葉子與根部的父節點的紅黑樹。
如:
旋轉:
兩種旋轉: 左旋和右旋。
如:
當在某個結點x上做左旋時,我們假設它的右孩子y不是nil[T]; x可以為樹內任意右孩子不是nil[T]的結點。
左旋過後,使y的父結點從x變為x的父結點,x變為y的右子結點,y的左子結點變為x的右子結點。
虛擬碼:
LEFT-ROTATE(T,x)
y <- right[x]
right[x] <- left[y]
if left[y]!= nil[T]
p[left[y]] <- x
p[y] <- p[x]
if p[x] == nil[T]
then root[T] <- y
else if x == left[p[x]]
then left[p[x]] <- y
else right[p[x]] <- y
left[y] <- x
p[x] <- y
RIGHT-ROTATE(T,x)
y <- left[x]
left[x] <- right[y]
if right[y] != nil[T]
p[right[y]] <- x
p[y] <- p[x]
if p[x] == nil[T]
then root[T] <- y
else if x == left[p[x]]
then left[p[x]] <- y
else right[p[x]] <- y
right[y] <- x
p[x] <- y
下面舉一個例子:
插入:
向一棵含n 個結點的紅黑樹中插入一個新結點的操作可在O(lgn) 時間內完成。
我們利用TREE-INSERT 過程的一個略作修改的版本,來將結點z插入樹T內,就好像T是一棵普通的二叉查詢樹一樣,然後將z著為紅色。為保證紅黑性質能繼續保持,我們呼叫一個輔助程式RB-INSERT-FIXUP 來對結點重新著色並旋轉。
紅黑樹有以下五點性質:
1、每個節點或是紅色的,或是黑色的;
2、根節點是黑色的;
3、每個葉節點(NIL)是黑色的;
4、如果一個節點是紅色的,則其兩個子節點都是黑色的;
5、對於每個節點,從該節點到其所有後代葉節點的簡單路徑上,均包含相同數目的黑色節點。
z插入後有以下幾種情況:
z作為根節點:(破壞了性質2)修改根節點為黑色即可;
z的父節點為黑色: 不做操作;
z的父節點為紅色:(破壞了性質4)又分成三種情況:
1、z的叔結點y是紅色;
2、z的叔結點y是黑色且z是一個右孩子;
3、z的叔結點y是黑色且z是一個左孩子。
情況1、z的叔結點y是紅色:
如:
如果z的叔節點是紅色,則z結點的祖父結點是黑色,父結點是紅色(父結點和本身都是紅色才會違背性質4)。我們只需要將父、叔結點塗成黑色,祖父結點塗成紅色,而本身顏色不需要改變。
這樣一來只是維護好了自己的“小家庭”,包括父、叔結點和祖父節點。而且保證了性質⑤:紅變黑、黑變紅,並不會增減某一條路徑中黑結點的個數。至於祖父節點的變色可能會導致它和它父親結點的衝突,所以我們就將“當前結點”這個“不合群”的帽子扣在了祖父結點,讓它繼續地被維護。
情況2、z的叔結點y是黑色且z是一個右孩子:
將z的父結點做為新的當前結點,將新的當前結點做左旋。這樣就將情況2轉到情況3來處理。
情況3:z的叔結點y是黑色且z是一個左孩子:
如:
如圖,情況2中將z的父結點做為新的當前結點,將新的當前結點做左旋。這樣就變成了情況3。再將此時的z父結點做為新的當前結點,將新的當前結點塗成黑色(之前是紅色),新的當前節點的父節點塗成紅色,再對新的當前結點的父結點做右旋。這樣就保證了性質5,同時修正了性質4,中間並沒有破壞任何其他性質,結束迴圈判斷。
虛擬碼:
RB-INSERT(T,x)
y <- nil[T]
x <- root[T]
while x != nil[T]
do y <- x
if key[z] < key[x]
then x <- left[x]
else x <- right[x]
p[z] <- y
if y == nil[T]
then root[T] <- z
else if key[z] < key[y]
then left[y] <- z
else right[y] <- z
left[z] <- nil[T]
right[z] <- nil[T]
color[z] <- RED
RB-INSERT-FIXUP(T,z)
RB-INSERT-FIXUP(T,z)
while color[p[z]] == RED
do if p[z] == left[p[p[z]]]
then y <- right[p[p[z]]]
if color[y] == RED
then color[p[z]] <- BLACK
color[y] <- BLACK
color[p[p[z]]] <- RED
z <- p[p[z]]
else if z == right[[p[z]]
then z <- p[z]
LEFT-ROTATE(T,x)
color[p[z]] <- BLACK
color[p[p[z]]] <- RED
RIGHT-ROTATE(T,z.p.p)
else(same as then clause with "right" and "left" exchanged)
color[root[T]] <- BLACK
刪除:
和n個結點的紅黑樹上的其他基本操作一樣,對一個結點的刪除要花O(lgn) 時間。
程式RB-DELETE是對TREE-DELETE程式略作修改得來的。在刪除一個結點後,該程式就呼叫一個輔助程式RB-DELETE-FIXUP,用來改變結點的顏色並做旋轉,從而保持紅黑樹性質。
刪除操作也分幾個步驟:
首先,按照搜尋二叉樹的刪除操作刪除要刪的結點,然後針對每種情況做換色和旋轉操作,使其恢復紅黑樹的性質。
紅黑樹有以下五點性質:
1、每個節點或是紅色的,或是黑色的;
2、根節點是黑色的;
3、每個葉節點(NIL)是黑色的;
4、如果一個節點是紅色的,則其兩個子節點都是黑色的;
5、對於每個節點,從該節點到其所有後代葉節點的簡單路徑上,均包含相同數目的黑色節點。
換色和旋轉的維護工作分為4種情況考慮(以下情況是x作為其父節點的左孩子的情況,右孩子的情況對稱處理):
1、待刪結點x的兄弟結點w是紅色:
如圖,x的兄弟結點w是紅色,所以它一定有兩個黑色的子結點。我們只需改變待刪的兄弟結點為黑色,改變父結點的顏色為紅色,然後再對父結點做一次左旋操作,所以待刪結點的兄弟結點一定是黑色。這樣就將情況1轉為情況2、3、4之一去處理。
2、待刪結點x的兄弟結點w是黑色,且w的兩個子結點都是黑色:
如圖,x的兄弟節點w是黑色,且w的兩個子節點都是黑色。這種情況下B左邊黑色高度比右邊的少1,所以只需將w變成紅色就滿足性質5。
如果父節點B本身是黑色的,這時父節點B具有黑黑的屬性,繼續當做x迭代處理!
如果父節點B本身是紅色的,這時父節點B具有紅黑的屬性,直接退出迴圈,把父節點B塗成黑色即可!
3、待刪結點x的兄弟結點w是黑色,且w的左孩子是紅色,右孩子是黑色:
我們需要交換w和其左孩子的顏色,即C、D顏色互換。然後對w進行右旋,使得待刪結點x的新兄弟結點new w是一個有紅色右孩子的黑色結點,這樣將情況3轉為情況4去處理。
4、待刪結點x的兄弟結點w是黑色,且w的右孩子是紅色:
此時我們可以將兄弟結點D染成當前父結點B的顏色,把當前父結點B的顏色染成黑色,兄弟結點的右子結點E染成黑色。然後對當前父結點B做一次左旋即可。
虛擬碼:
RB-TRANSPLANT(t,u,v)
if u.p == T.nil
T.root = v
else if u == u.p.left
u.p.left = v
else u.p.right = v
v.p = u.p
RB-DELETE(T,z)
y = z
y-original-color = y.color
if z.left == T.nil
x = z.right
RB-TRANSPLANT(T,z,x)
else if z.right == T.nil
x = z.left
RB-TRANSPLANT(T,z,x)
else y = TREE-MINIMUM(z.right)
y-original-color = y.color
x = y.right
if y.p == z
x.p = y
else
RB-TRANSPLANT(T,y,x)
y.right = z.right
z.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 && x.color == BLACK
if x == x.p.left
w = x.p.right
if w.color == RED
w.color = BLACK
x.p.color = RED
LEFT_ROTATE(T,x.p)
w = x.p.right
if w.left.color == BLACK && w.right.color == BLACK
w.color = RED
x = x.p
else
if w.right.color == BLACK
w.left.color = BLACK
w.color = RED
RIGHT-ROTATE(T,w)
w = x.p.right
w.color = x.p.color
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)
T.root.color = BLACK