http://www.cnblogs.com/sungoshawk/p/3740411.html
算法導論讀書筆記(13)
目錄
紅黑樹
紅黑樹 是一種二叉查找樹,但在每個結點上增加了一個存儲位表示結點的顏色,可以是 RED
或 BLACK
。通過對任何一條從根到葉子的路徑上的各個結點着色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡的。紅黑樹(red-black tree)是許多“平衡的”查找樹中的一種,它能保證在最壞情況下,基本的動態集合操作的時間爲 O (
lg n )。
樹中每個結點包含五個域: color , key , left , right 和 p 。如果某結點沒有一個子結點或父結點,則該結點相應的指針爲 NIL
。我們將這些 NIL
視爲指向二叉查找樹外結點(葉子)的指針,而把帶關鍵字的結點視爲樹的內結點。
一棵紅黑樹需要滿足下面的 紅黑性質 :
- 每個結點或是紅的,或是黑的。
- 根結點是黑的。
- 每個葉結點(
NIL
)是黑的。 - 如果一個結點是紅的,則它的兩個孩子都是黑的。
- 對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。
下圖給出了一棵紅黑樹的例子。
爲了便於處理紅黑樹代碼中的邊界條件,我們採用一個哨兵來代表 NIL
。對一棵紅黑樹來說,哨兵 T.nil 是一個與樹內普通結點有相同域的對象。它的 color域爲 BLACK
,而其他域可以設爲任意允許的值。如下圖所示,所有指向 NIL
的指針都被替換成指向哨兵 T.nil 的指針。
使用哨兵後,可以將結點 x 的 NIL
孩子視爲一個其父結點爲 x 的普通結點。這裏我們用一個哨兵 T.nil 來代表所有的 NIL
(所有的葉子以及根部的父結點)。
通常我們將注意力放在紅黑樹的內部結點上,因爲它們存儲了關鍵字的值。因此本文其餘部分都將忽略紅黑樹的葉子,如下圖所示。
從某個結點 x 出發(不包括該結點)到達一個葉結點的任意一條路徑上,黑色結點的個數稱爲該結點 x 的 黑高度 ,用bh( x )表示。紅黑樹的黑高度定義爲其根結點的黑高度。
引理
一棵有 n 個內結點的紅黑樹的高度至多爲2lg( n + 1 )。
旋轉
當在含 n 個關鍵字的紅黑樹上運行時,查找樹操作 TREE-INSERT
和 TREE-DELETE
的時間爲 O ( lg n )。由於這兩個操作對樹做了修改,結果可能違反了紅黑樹的性質,爲保持紅黑樹的性質,就要改變樹中某些結點的顏色和指針結構。
指針結構的修改是通過 旋轉 來完成的,這是一種能保持二叉查找樹性質的查找樹局部操作。下圖給出了兩種旋轉:左旋和右旋。
當在某個結點 x 上做左旋時,我們假設它的右孩子 y 不是 T.nil ; x 可以爲樹內任意右孩子不是 T.nil 的結點。左旋以 x 到 y 之間的鏈爲“支軸”進行。它使 y稱爲該子樹新的根, x 成爲 y 的左孩子,而 y 的左孩子則成爲 x 的右孩子。
在 LEFT-ROTATE
的僞碼中,假設 x.right != T.nil ,並且根的父結點是 T.nil 。
LEFT-ROTATE(T, x) 1 y = x.right // set y 2 x.right = y.left // turn y's left subtree into s's right subtree 3 if y.left != T.nil 4 y.left.p = x 5 y.p = x.p 6 if x.p == T.nil 7 T.root = y 8 elseif x == x.p.left 9 x.p.left = y 10 else 11 x.p.right = y 12 y.left = x // put x on y's left 13 x.p = y
下圖顯示了 LEFT-ROTATE
的操作過程。 RIGHT-ROTATE
的程序是對稱的。它們都在 O ( 1 )時間內完成。
RIGHT-ROTATE(T, x) 1 y = x.left 2 x.left = y.right 3 if y.right != T.nil 4 y.right.p = x 5 y.p = x.p 6 if x.p == T.nil 7 T.root = y 8 elseif x == x.p.left 9 x.p.left = y 10 else 11 x.p.right = right 12 y.right = x 13 x.p = y
插入
向一棵含 n 個結點的紅黑樹 T 中插入一個新結點 z 的操作可在 O ( lg n )時間內完成。首先將結點 z 插入樹 T 中,就好像 T 是一棵普通的二叉查找樹一樣,然後將 z 着爲紅色。爲保證紅黑性質,這裏要調用一個輔助程序 RB-INSERT-FIXUP
來對結點重新着色並旋轉。調用 RB-INSERT
會將 z 插入紅黑樹 T內,假設 z 的 key 域已經事先被賦值。
RB-INSERT(T, z) 1 y = T.nil 2 x = T.root 3 while x != T.nil 4 y = x 5 if z.key < x.key 6 x = x.left 7 else 8 x = x.right 9 z.p = y 10 if y == T.nil 11 T.root = z 12 elseif z.key < y.key 13 y.left = z 14 else 15 y.right = z 16 z.left = T.nil 17 z.right = T.nil 18 z.color = RED 19 RB-INSERT-FIXUP(T, z)
過程 RB-INSERT
的運行時間爲 O ( lg n )。過程 TREE-INSERT
和 RB-INSERT
之間有四處不同。首先,在 TREE-INSERT
內的所有的 NIL
都被 T.nil 代替。其次,在 RB-INSERT
的第16,17行中,設置 z.left 和 z.right 爲 T.nil ,來保持正確的樹結構。第三,在第18行將 z 着爲紅色。第四,在最後一行,調用 RB-INSERT-FIXUP
來保持紅黑性質。
RB-INSERT-FIXUP(T, z) 1 while z.p.color == RED 2 if z.p == z.p.p.left // z的父結點是其父結點的左孩子 3 y = z.p.p.right // 令y爲z的叔父結點 4 if y.color == RED 5 z.p.color = BLACK // case 1 6 y.color = BLACK // case 1 7 z.p.p.color = RED // case 1 8 z = z.p.p // case 1 9 else 10 if z == z.p.right 11 z = z.p // case 2 12 LEFT-ROTATE(T, z) // case 2 13 z.p.color = BLACK // case 3 14 z.p.p.color = RED // case 3 15 RIGHT-ROTATE(T, z.p.p) // case 3 16 else // z的父結點是其父結點的右孩子 17 y = z.p.p.left // 令y爲z的叔父結點 18 if y.color = RED 19 z.p.color = BLACK 20 y.color = BLACK 21 z.p.p.color = RED 22 z = z.p.p 23 else 24 if z = z.p.left 25 z = z.p 26 RIGHT-ROTATE(T, z) 27 z.p.color = BLACK 28 z.p.p.color = RED 29 LEFT-ROTATE(T, z.p.p) 30 T.root.color = BLACK
下圖顯示了在一棵紅黑樹上 RB-INSERT-FIXUP
是如何操作的。
要理解 RB-INSERT-FIXUP
的工作過程,需要分三個主要步驟來分析其代碼。首先,確定當結點 z 被插入並着色爲紅色後,紅黑性質有哪些不能保持。其次,分析 while
循環的總目標。最後,具體分析 while
循環中的三種情況。
在調用 RB-INSERT-FIXUP
時,紅黑性質中的性質1和性質3會繼續成立,因爲新插入結點的子女都是哨兵 T.nil 。性質5也會成立,因爲結點 z 代替了(黑色)哨兵,且結點 z 本身是具有哨兵子女的紅色結點。因此,可能被破壞的就是性質2和性質4。這是因爲 z 被着爲紅色,如果 z 是根結點則破壞了性質2,如果 z 的父結點是紅色則破壞了性質4。上圖a顯示在結點 z 被插入後性質4被破壞。
要保持樹的紅黑性質,實際上一共要考慮六種情況,但其中三種與另外三種是對稱的,區別在於 z 的父結點 z.p 是 z 的祖父結點 z.p.p 的左孩子還是右孩子。這裏只討論 z.p 是左孩子的情況。
上面僞碼中情況1和情況2,3的區別在於 z 的叔父結點的顏色有所不同。如果 y 是紅色,則執行情況1。否則,控制轉移到情況2和情況3上。在所有三種情況中, z 的祖父 z.p.p 都是黑色的,因爲它的父結點 z.p 是紅色的,故性質4只在 z 和 z.p 之間被破壞了。
情況1 : z 的叔父結點 y 是紅色的
下圖顯示的是情況1(第5~8行)的狀況。只有在 z.p 和 y 都是紅色的時候才執行。既然 z.p.p 是黑色的,我們可以將 z.p 和 y 都着爲黑色以解決 z 和 z.p都是紅色的問題,將 z.p.p 着爲紅色以保持性質5。然後把 z.p.p 當作新增的結點 z 來重複 while
循環。指針 z 在樹中上移兩層。
情況2 : z 的叔父結點 y 是黑色的,而且 z 是右孩子
情況3 : z 的叔父結點 y 是黑色的,而且 z 是左孩子
在情況2和情況3中, z 的叔父結點 y 是黑色的。這兩種情況通過 z 是 z.p 的左孩子還是右孩子來區別。在情況2中,結點 z 是其父結點的右孩子。我們立刻使用一個左旋來將此狀況轉變爲情況3,此時結點 z 成爲左孩子。因爲 z 和 z.p 都是紅色的,所以所做的旋轉對結點的黑高度和性質5都無影響。至此, z的叔父結點 y 總是黑色的,另外 z.p.p 存在且其身份保持不變。在情況3中,要改變某些結點的顏色,並作一次右旋以保持性質5。這樣,由於在一行中不再有兩個連續的紅色結點,所有的處理到此結束。
刪除
和 n 個結點的紅黑樹上的其它基本操作一樣,對一個結點的刪除要花 O ( lg n )時間。
首先,我們需要自定義一個類似於 TREE-DELETE
中調用的 TRANSPLANT
的子程序。該過程接收三個參數,紅黑樹 T 以及兩棵子樹 u , v 。過程用子樹 v 來替代子樹 u 在樹中的位置。
RB-TRANSPLANT(T, u, v) 1 if u.p == T.nil 2 T.root = v 3 elseif u == u.p.left 4 u.p.left = v 5 else 6 u.p.right = v 7 v.p = u.p
過程 RB-TRANSPLANT
和 TRANSPLANT
有兩點不同。首先,第1行使用哨兵 T.nil 替代 NIL
。其次,第7行的賦值語句不再需要條件。
過程 RB-DELETE
同 TREE-DELETE
類似,但是多了些代碼。有些代碼用於跟蹤記錄可能破壞紅黑性質的結點 y 的狀態。如果待刪除的結點 z 的孩子結點少於兩個,那麼可以直接從樹中刪除 z ,並讓 y 等於 z 。如果待刪除的結點 z 有兩個孩子,令 y 爲 z 的後繼,並用 y 替代 z 在樹中的位置。我們還要記住 y 在刪除或移動之前的顏色。由於結點 x 也可能破壞樹的紅黑性質,我們也需要跟蹤記錄下這個佔據了結點 y 最初位置的結點 x 的狀態。刪除結點 z 後,過程RB-DELETE
還要調用 RB-DELETE-FIXUP
以保持紅黑性質。
RB-DELETE(T, z) 1 y = z 2 y-original-color = y.color 3 if z.left == T.nil 4 x = z.right 5 RB-TRANSPLANT(T, z, z.right) 6 elseif z.right == T.nil 7 x = z.left 8 RB-TRANSPLANT(T, z, z.left) 9 else 10 y = TREE-MINIMUM(z.right) 11 y-original-color = y.color 12 x = y.right 13 if y.p == z 14 x.p = y 15 else 16 RB-TRANSPLANT(T, y, y.right) 17 y.right = z.right 18 y.right.p = y 19 RB-TRANSPLANT(T, z, y) 20 y.left = z.left 21 y.left.p = y 22 y.color = z.color 23 if y-original-color == BLACK 24 RB-DELETE-FIXUP(T, x)
RB-DELETE
和 TREE-DELETE
主要的不同之處羅列如下:
- 我們維護了一個結點 y 。第1行令 y 指向了結點 z (此時 z 爲待刪結點且它的孩子結點少於兩個)。當 z 有兩個孩子結點時,第10行令 y 指向 z 的後繼,然後 y 會取代 z 在樹中的位置。
- 由於 y 的顏色可能發生變化,變量 y-original-color 保存了 y 在發生改變之前的顏色。在爲 y 賦值後,第2行和第10行立刻設置了該變量。如果 z 有兩個孩子結點,那麼 y != z 並且 y 會佔據結點 z 在紅黑樹中的初始位置;第22行將 y 的顏色設置成和 z 一樣。我們需要保存 y 的初始顏色以便在過程
RB-DELETE
結尾處做測試;如果它是黑色的,那麼刪除或移動結點 y 就會破壞紅黑性質。 - 我們還要跟蹤記錄結點 x 的狀態。第4,7和12行的賦值語句令 x 指向 y 的孩子結點或哨兵 T.nil 。
- 一旦結點 x 移入 y 的初始位置,屬性 x.p 總是指向 y 的父結點,哪怕 x 是哨兵 T.nil 也一樣。除非 z 是 y 的父結點(此時 z 有兩個孩子且 y 是它的右孩子)。對 x.p 的賦值操作在過程
RB-TRANSPLANT
中第7行執行(通過觀察可以看出來,在第5,8和16行被調用的RB-TRANSPLANT
,其傳遞的第二個參數就是 x )。 - 最後,如果結點 y 是黑色的,我們可能會破壞某些紅黑性質,這就需要調用
RB-DELETE-FIXUP
來保持紅黑性質。
在 RB-DELETE
中,如果被刪除的結點 y 是黑色的,則會產生三個問題。首先,如果 y 原來是根結點,而 y 的某個紅色孩子成爲了新的根,這就違反了性質2。其次,如果 x 和 x.p 都是紅色的,就違反了性質4。第三,刪除 y 可能導致其路徑上黑結點的個數少1,這就違反了性質5。補救的一個辦法就是把結點 x視爲還有額外一重黑色。即,如果將任意包含結點 x 的路徑上黑結點個數加1,則性質5成立。當將黑結點 y 刪除時,將其黑色“下推”至其子結點。這樣問題變爲結點 x 可能既不是紅色,也不是黑色,從而違反了性質1。這時需要調用 RB-DELETE-FIXUP
來糾正。
RB-DELETE-FIXUP(T, x) 1 while x != T.root and x.color == BLACK 2 if x == x.p.left 3 w = x.p.right 4 if w.color == RED 5 w.color = BLACK // case 1 6 x.p.color = RED // case 1 7 LEFT-ROTATE(T, x.p) // case 1 8 w = x.p.right // case 1 9 if w.left.color == BLACK and w.right.color == BLACK 10 w.color = RED // case 2 11 x = x.p // case 2 12 else 13 if w.right.color == BLACK 14 w.left.color = BLACK // case 3 15 w.color = RED // case 3 16 RIGHT-ROTATE(T, w) // case 3 17 w = x.p.right // case 3 18 w.color = x.p.color // case 4 19 x.p.color = BLACK // case 4 20 w.right.color = BLACK // case 4 21 LEFT-ROTATE(T, x.p) // case 4 22 x = T.root // case 4 23 else (same as then clause with "right" and "left" exchanged) 24 x.color = BLACK
過程 RB-DELETE-FIXUP
可以恢復性質1,2和4。這裏僅說明性質1。過程中 while
循環的目標是將額外的黑色沿樹上移,直到:
- x 指向一個紅黑結點,此時,在第24行,將 x 着爲黑色;
- x 指向根,這是可以簡單地消除額外的黑色,或者
- 做必要的旋轉和顏色改變。
在 while
循環中, x 總是指向具有雙重黑色的那個非根結點。用 w 表示 x 的兄弟。算法中的四種情況在下圖中加以說明。首先要說明的是在每種情況中的變換是如何保持性質5的。關鍵思想就在每種情況下,從(其包括)子樹的根到每棵子樹之間的黑結點個數(包括 x 的額外黑色)並不被變換所改變。因此,性質5在變換之前成立,之後依然成立。
情況1 : x 的兄弟 w 是紅色的
見 RB-DELETE-FIXUP
第5~8行和上圖a。因爲 w 必須有紅色孩子,我們可以改變 w 和 x.p 的顏色,再對 x.p 做一次左旋,而且紅黑性質得以繼續保持, x的新兄弟是旋轉之前 w 的某個孩子,其顏色爲黑色。這樣,情況1就轉換成情況2,3或4。
情況2 : x 的兄弟 w 是黑色的,且 w 的兩個孩子都是黑色的
見 RB-DELETE-FIXUP
第10~11行和上圖b。因爲 w 和兩個孩子都是黑色的,故從 x 和 w 上各去掉一重黑色,從而 x 只有一重黑色而 w 爲紅色。爲了補償去掉的黑色,需要在原 x.p 內新增一重額外黑色。然後新結點 x 在最後被着爲黑色。
情況3 : x 的兄弟 w 是黑色的, w 的左孩子是紅色的,右孩子是黑色的
見 RB-DELETE-FIXUP
第14~17行和上圖c。此時可以交換 w 和其左孩子 w.left 的顏色,並對 w 右旋,而紅黑性質依然保持,且從情況3轉換成了情況1。
情況4 : x 的兄弟 w 是黑色的,且 w 的右孩子是紅色的
見 RB-DELETE-FIXUP
第18~22行和上圖d。通過做顏色的修改並對 x.p 做一次左旋,可以去掉 x 的額外黑色並把它變成單獨黑色。將 x 置爲根後, while
會在測試其循環條件時結束。