算法導論讀書筆記(13)
目錄
紅黑樹
紅黑樹 是一種二叉查找樹,但在每一個結點上增長了一個存儲位表示結點的顏色,能夠是 RED
或 BLACK
。經過對任何一條從根到葉子的路徑上的各個結點着色方式的限制,紅黑樹確保沒有一條路徑會比其餘路徑長出兩倍,於是是接近平衡的。紅黑樹(red-black tree)是許多「平衡的」查找樹中的一種,它能保證在最壞狀況下,基本的動態集合操做的時間爲 O ( lg n )。html
樹中每一個結點包含五個域: color , key , left , right 和 p 。若是某結點沒有一個子結點或父結點,則該結點相應的指針爲 NIL
。咱們將這些 NIL
視爲指向二叉查找樹外結點(葉子)的指針,而把帶關鍵字的結點視爲樹的內結點。算法
一棵紅黑樹須要知足下面的 紅黑性質 :測試
- 每一個結點或是紅的,或是黑的。
- 根結點是黑的。
- 每一個葉結點(
NIL
)是黑的。 - 若是一個結點是紅的,則它的兩個孩子都是黑的。
- 對每一個結點,從該結點到其子孫結點的全部路徑上包含相同數目的黑結點。
下圖給出了一棵紅黑樹的例子。3d
爲了便於處理紅黑樹代碼中的邊界條件,咱們採用一個哨兵來表明 NIL
。對一棵紅黑樹來講,哨兵 T.nil 是一個與樹內普通結點有相同域的對象。它的 color 域爲 BLACK
,而其餘域能夠設爲任意容許的值。以下圖所示,全部指向 NIL
的指針都被替換成指向哨兵 T.nil 的指針。指針
使用哨兵後,能夠將結點 x 的 NIL
孩子視爲一個其父結點爲 x 的普通結點。這裏咱們用一個哨兵 T.nil 來表明全部的 NIL
(全部的葉子以及根部的父結點)。code
一般咱們將注意力放在紅黑樹的內部結點上,由於它們存儲了關鍵字的值。所以本文其他部分都將忽略紅黑樹的葉子,以下圖所示。htm
從某個結點 x 出發(不包括該結點)到達一個葉結點的任意一條路徑上,黑色結點的個數稱爲該結點 x 的 黑高度 ,用bh( x )表示。紅黑樹的黑高度定義爲其根結點的黑高度。對象
引理
一棵有 n 個內結點的紅黑樹的高度至多爲2lg( n + 1 )。
blog
旋轉
當在含 n 個關鍵字的紅黑樹上運行時,查找樹操做 TREE-INSERT
和 TREE-DELETE
的時間爲 O ( lg n )。因爲這兩個操做對樹作了修改,結果可能違反了紅黑樹的性質,爲保持紅黑樹的性質,就要改變樹中某些結點的顏色和指針結構。 get
指針結構的修改是經過 旋轉 來完成的,這是一種能保持二叉查找樹性質的查找樹局部操做。下圖給出了兩種旋轉:左旋和右旋。
當在某個結點 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
會在測試其循環條件時結束。