紅黑樹簡介:
紅黑樹是一棵二叉搜索樹,它在每個結點上增加了一個存儲位來表示結點的顏色,可以是RED 或 BLACK。通過對任何一條根到葉子的簡單路徑上各個結點的顏色進行約束,紅黑樹確保沒有一條路徑回避其他路徑長處2倍,因而是近似平衡的。
樹的每個結點包含 5 個屬性:color,key,left,right和p。如果一個結點沒有子結點或者父結點,則該結點相應的指針屬性的值為NULL。我們可以把這些NULL視為指向二叉搜索樹葉結點的指針,而把帶關鍵字的結點視為樹的內部結點。
紅黑樹的性質:
一棵紅黑樹是滿足下面紅黑性質的二叉搜索樹:
1.每個結點或是紅色的,或是黑色的
2.根節點是黑色的
3.每個葉結點(NULL)是黑色的
4.如果一個結點是紅色的,那么他的兩個子結點都是黑色的
5.對於每個結點,從該結點到其所有后代葉結點的簡單路徑上,包含相同數目的黑色結點
這 5 個性質中1,2,4都比較好理解。3與我們常說的(大部分數據結構書上說的)葉結點有一點點區別,如下圖:
那性質5又是什么意思呢?我們再來看一個圖:
由紅黑樹的 5 個性質可知,上幅圖中左圖是紅黑樹,而右圖非紅黑樹。右圖中滿足紅黑樹的性質1.2.3.4,但是不滿足性質5:從根節點6(不包括根節點)到各葉結點的簡單路徑上的黑色黑色結點個數並不相等。例如:6-1有2個,而6-8和6-10都是有三個。
這些約束強制了紅黑樹的關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長。結果是這個樹大致上是平衡的。因為操作比如插入、刪除和查找某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查找樹。
要知道為什么這些特性確保了這個結果,注意到屬性4導致了路徑不能有兩個毗連的紅色節點就足夠了。最短的可能路徑都是黑色節點,最長的可能路徑有交替的紅色和黑色節點。因為根據屬性5所有最長的路徑都有相同數目的黑色節點,這就表明了沒有路徑能多於任何其他路徑的兩倍長。
在很多樹數據結構的表示中,一個節點有可能只有一個子節點,而葉子節點包含數據。用這種范例表示紅黑樹是可能的,但是這會改變一些屬性並使算法復雜。為此,本文中我們使用 "nil 葉子" 或"空(null)葉子",如上圖所示,它不包含數據而只充當樹在此結束的指示。這些節點在繪圖中經常被省略,導致了這些樹好像同上述原則相矛盾,而實際上不是這樣。與此有關的結論是所有節點都有兩個子節點,盡管其中的一個或兩個可能是空葉子。
紅黑樹的操作:
因為每一個紅黑樹也是一個特化的二叉查找樹,因此紅黑樹上的只讀操作與普通二叉查找樹上的只讀操作相同。然而,在紅黑樹上進行插入操作和刪除操作會導致不再符合紅黑樹的性質。恢復紅黑樹的屬性需要少量(O(log n))的顏色變更(實際是非常快速的)和不超過三次樹旋轉(對於插入操作是兩次)。雖然插入和刪除很復雜,但操作時間仍可以保持為 O(log n) 次。我們在這只講講紅黑樹的插入和刪除。
1.插入
下面看看算法導論中給的偽代碼:
1 /* 2 注意以下的T.nil,是一個與普通紅黑樹結點相同的對象。他的color是BLACK,他也是根節點的父節點 3 RB-INSERT(T,z) //向樹T中增加結點z 4 y = T.nil //根節點的父節點 5 x = T.root //根節點 6 while x != T.nil //while循環內是為了尋找插入結點z的位置 7 y = x //y始終是x的父節點 8 if z.key < x.key 9 x = x.left 10 else 11 x = x.right 12 //跳出while循環之后,說明y結點的某個孩子是T.nil了,可以插入了! 13 z.p = y //z的父結點是y 14 if y == T.nil //如果y就是 T.nil說明該樹為空,插入z后,z就是根節點 15 T.root = z 16 else if z.key < y.key //如果z比y結點值小,則插到y的左孩子上 17 y.left = z 18 else 19 y.right = z //否則插到y的右孩子上 20 z.left = T.nil 21 z.right = T.nil //將z的左右孩子都設為T.nil 22 z.color = RED //z的顏色設為紅色 23 RB-INSERT-FIXUP(T,Z) //插入一個紅色結點會破壞紅黑樹的性質,需要調整 24 */
比如我們插入一個值為3的結點:在RB-INSERT-FIXUP函數執行之前,執行的結果如下圖:
由上圖可以看出T.nil的作用是充當一個哨兵,它也是一個紅黑樹結點對象,且顏色為黑色,其他的值任意!插入3,並將3的顏色塗成紅色之后,有可能會破壞紅黑樹的性質2和4(上圖就破壞了性質5).所以我們要調用RB-INSERT-FIXUP來保持紅黑樹的性質。RB-INSERT-FIXUP的偽代碼如下:
1 /* 2 以下是實現RB-INSERT-FIXUP(T,Z)偽代碼 3 while z.p.color == RED //因為z本身是紅色,如果他的父結點是紅色那這個循環就要繼續---調節樹 4 if z.p == z.p.p.left //如果z的父親是z祖父的左孩子 5 y = z.p.p.right //令y為z祖父的右孩子,也就是說y是z的叔叔 6 if y.color == RED //如果y的顏色是紅色 7 z.p.color = BLACK //case 1 既然z是紅色,為了不破壞性質4,將z的父節點塗成黑色 8 y.color = BLACK //case 1 同時也要講z的叔叔結點塗成黑色 9 z.p.p.color=RED //case 1 同時將z的祖父結點(y的父節點)塗成紅色 10 z = z.p.p //case 1 令z 等於 z的祖父,循環繼續 11 else if z == z.p.right //如果z是父結點的右孩子 12 z = z.p //case 2 z等於z的父結點 13 LEFT-ROTATE(T,Z) //case 2 右旋 14 z.p.color = BLACK //case 3 將z的父結點顏色塗成黑色 15 z.p.p.color = RED //case 3 將z的祖父結點塗成紅色 16 RIGHT-ROTATE(T,Z.P.P) //case 3 右旋 17 else(same as then clause with 'right' and 'left' exchanged) 18 T.root.color = BLACK 19 */
這里偽代碼里面有兩個函數要注意下,LEFT-ROTATE() 和 RIGHT-ROTATE().這個分別是左旋和右旋的函數。左旋和右旋的過程我已經在我的另一篇博客中用圖解釋的很清楚了:http://www.cnblogs.com/zhuwbox/p/3636783.html。
下面是左旋的偽代碼:
1 /* 2 LEFT-ROTATE(T,x)--參考上圖 3 y = x.right //給y賦值 4 x.right = y.left //將x的右結點指向y的左結點 5 if y.left != T.nil 6 y.left.p = x //設置y左結點的父節點為x 7 y.p = x.p //y的父結點是x的父節點 8 if x.p == T.nil //如果 x 是根節點 9 T.root = y; 10 elseif x == x.p.left //如果x是父結點的左孩子 11 x.p.left = y; // 12 else x.p.right = y //如果x是父結點的右孩子 13 y.left = x; //y的左孩子是x 14 x.p = y //x的父節點是y 15 */
RB-INSERT-FIXUP要處理的情況有三種。
a).情況一:插入結點后的結點z。z和父結點都是紅色,違反性質4.如下圖:
解決方法是:將z的父結點和叔叔結點塗成黑色,並且z的指針沿z樹上升(對應RB-INSERT-FIXUP代碼中的case 1部分)。所得情況如下圖
b).情況二:調整后的結點z(此時是7)和父結點(結點2)都是紅色,但是叔叔結點(結點1)是黑色,此時出現情況二。解決方法:將2作為根節點T進行左旋。得到如下圖:
c).情況三:調整后的結點z(此時是2)和父結點是紅色,但是叔叔結點(8)是黑色。要進行如下操作:將z結點的父結點塗成黑色,將z的祖父結點塗成紅色。再以z的父結點為根T,作一次右旋轉即可得到一棵合法的紅黑樹,如下圖:
此時的z的父節點不再是紅色,退出while循環(如果不退出循環,情況肯定是這三種中的一種)。一棵合法的紅黑樹形成!