資料結構樹之紅黑樹
紅黑樹簡介:
紅黑樹是一棵二叉搜尋樹,它在每個結點上增加了一個儲存位來表示結點的顏色,可以是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迴圈(如果不退出迴圈,情況肯定是這三種中的一種)。一棵合法的紅黑樹形成!