算法導論讀書筆記(13)

http://www.cnblogs.com/sungoshawk/p/3740411.html

算法導論讀書筆記(13)

紅黑樹

紅黑樹 是一種二叉查找樹,但在每個結點上增加了一個存儲位表示結點的顏色,可以是 RED 或 BLACK 。通過對任何一條從根到葉子的路徑上的各個結點着色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出兩倍,因而是接近平衡的。紅黑樹(red-black tree)是許多“平衡的”查找樹中的一種,它能保證在最壞情況下,基本的動態集合操作的時間爲 O ( lg n )。

樹中每個結點包含五個域: color , key , left , right 和 p 。如果某結點沒有一個子結點或父結點,則該結點相應的指針爲 NIL 。我們將這些 NIL 視爲指向二叉查找樹外結點(葉子)的指針,而把帶關鍵字的結點視爲樹的內結點。

一棵紅黑樹需要滿足下面的 紅黑性質 :

  1. 每個結點或是紅的,或是黑的。
  2. 根結點是黑的。
  3. 每個葉結點( NIL )是黑的。
  4. 如果一個結點是紅的,則它的兩個孩子都是黑的。
  5. 對每個結點,從該結點到其子孫結點的所有路徑上包含相同數目的黑結點。

下圖給出了一棵紅黑樹的例子。

爲了便於處理紅黑樹代碼中的邊界條件,我們採用一個哨兵來代表 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 循環的目標是將額外的黑色沿樹上移,直到:

  1. x 指向一個紅黑結點,此時,在第24行,將 x 着爲黑色;
  2. x 指向根,這是可以簡單地消除額外的黑色,或者
  3. 做必要的旋轉和顏色改變。

在 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 會在測試其循環條件時結束。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章