1. 程式人生 > >紅黑樹下——紅黑樹的實現

紅黑樹下——紅黑樹的實現

1. 實現紅黑樹的基本思想

實際上,紅黑樹是有固定的平衡過程的:遇到什麼樣的節點分佈,我們就對應怎麼去調整。只要按照這些固定的調整規則來操作,就能將一個非平衡的紅黑樹調整成平衡的。

首先,我們需要再來看一下紅黑樹的定義:

  • 根節點是黑色的;
  • 每個葉子節點都是黑色的空節點(NIL),也就是說,葉子節點不儲存資料;
  • 任何相鄰的節點都不能同時為紅色,也就是說,紅色節點是被黑色節點隔開的;
  • 每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點。

在插入、刪除節點的過程中,第三、四點要求可能會被破壞,所以“平衡調整”,實際上就是把被破壞的第三、四點恢復過來。

在調整過程中有兩個非常重要的操作,左旋(rotate left)和右旋(rotate right),左旋其實就是圍繞某個節點的左旋,而右旋就是圍繞某個節點的右旋

2. 插入操作的平衡調整

紅黑樹規定,插入的節點必須是紅色的。而且,二叉查詢樹中新插入的節點都是放在葉子節點上。所以,插入操作的平衡調整,有這樣兩種特殊情況:

  • 如果插入節點的父節點是黑色的,那我們什麼都不做,它仍然滿足紅黑樹的定義。
  • 如果插入的節點是根節點,那我們直接改變它的顏色,把它變成黑色就可以了。

除此之外,其它情況都會違背紅黑樹的定義,於是我們就需要進行調整,調整的過程包含兩種基本的操作:左右旋轉和改變顏色

紅黑樹的平衡調整是一個迭代的過程,我們把正在處理的節點叫作關注節點,關注結點會隨著迭代的進行而不斷變化,最初的關注節點就是新插入的節點。

新節點插入後,如果紅黑樹的平衡被打破,那一般會有下面三種情況。我們只需要根據每種情況的特點,不停地調整,就可以讓紅黑樹繼續保持平衡。

為了簡化描述,我們把父節點的兄弟節點叫作叔叔結點,父節點的父節點叫作祖父節點。

CASE 1 :如果關注節點是 a,它的叔叔結點 d 是紅色,我們就依次執行下面的操作

  • 將關注節點 a 的父節點 b 、叔叔結點 d 的顏色都設定成黑色;
  • 將關注節點 a 的祖父節點 c 的顏色設定成紅色;
  • 關注節點變成 a 的祖父節點 c;
  • 跳到 CASE 2 或者 CASE 3。

CASE 2 :如果關注節點是 a,它的叔叔結點 d 是黑色,關注節點 a 是其父結點 b 的右子節點,我們就依次執行下面的操作

  • 關注節點變成 a 的父節點 b;
  • 圍繞新的關注節點 b 左旋;
  • 跳到 CASE 3。

CASE 3 :如果關注節點是 a,它的叔叔結點 d 是黑色,關注節點 a 是其父結點 b 的左子節點,我們就依次執行下面的操作

  • 圍繞關注節點 a 的祖父節點 c 右旋;
  • 將關注節點 a 的父節點 b、兄弟節點 c 的顏色互換;
  • 調整結束。

3. 刪除操作的平衡調整

刪除操作的平衡調整分為兩步,第一步是針對刪除節點初步調整。初步調整是保證整棵紅黑樹在一個節點刪除之後,仍然滿足第四條的定義。第二步是針對關注節點進行二次調整,讓它滿足紅黑樹的的第三條定義。

3.1. 針對刪除節點初步調整

經過初步調整後,為了保證紅黑樹的第四條要求,有些節點會被標記為兩種顏色,“紅-黑” 或者 “黑-黑”,如果一個節點被標記為了 “黑-黑”,那在計算黑色節點個數的時候,要算成兩個黑色節點。

下面,如果一個節點既可以是紅色,也可以是黑色,我們用一半黑色一半紅色來表示。如果一個節點是 “紅-黑” 或者 “黑-黑”,我們用左上角的一個小黑點來表示。

CASE 1 :如果要刪除的節點是 a,它只有一個子節點 b,我們就依次執行下面的操作

  • 刪除節點 a,並且把節點 b 替換到節點 a 的位置,這一部分操作和普通的二叉查詢樹的刪除操作一樣;
  • 節點 a 只能是黑色,結點 b 也只能是紅色,其它情況均不符合紅黑樹的定義。這種情況下,我們把節點 b 改成黑色;
  • 調整結束,不需要進行二次調整;

CASE 2 :如果要刪除的節點 a 有兩個非空子節點,並且它的後繼節點就是它的右子節點 c,我們就依次執行下面的操作

  • 如果節點 a 的後繼節點就是右子節點 c,那右子節點 c 肯定沒有左子樹。我們把節點 a 刪除,並且將節點 c 替換到節點 a 的位置,這一部分操作和普通的二叉查詢樹的操作無異;
  • 然後把節點 c 的顏色設定為和節點 a 相同的顏色;
  • 如果節點 c 是黑色,為了不違反紅黑樹的第四條定義,我們給節點 c 的右子節點 d 多加一個黑色,這時候節點 d 就成了 “紅-黑” 或者 “黑-黑”;
  • 這時候,關注節點變成了節點 d,第二步的調整操作就會針對關注節點來做。

CASE 3 :如果要刪除的節點 a 有兩個非空子節點,並且它的後繼節點不是右子節點,我們就依次執行下面的操作

  • 找到後繼節點 d,並將它刪除,刪除後繼節點 d 的過程參照 CASE 1;
  • 將節點 a 替換成後繼節點 d;
  • 把節點 d 的顏色設定成和節點 a 相同的顏色;
  • 如果節點 d 是黑色,為了不違反紅黑樹的第四條定義,我們給節點 d 的右子節點 c 多加一個黑色,這時候節點 c 就成了 “紅-黑” 或者 “黑-黑”;
  • 這時候,關注節點變成了節點 c,第二步的調整操作就會針對關注節點來做。

3.2. 針對關注節點進行二次調整

經過初步調整之後,關注節點變成了 “紅-黑” 或者 “黑-黑” 節點,針對這個關注節點,我們再分四種情況來進行二次調整,二次調整是為了讓紅黑樹中不存在相鄰的紅色節點。

CASE 1 :如果關注節點是 a,它的兄弟節點 c 是紅色的,我們就依次執行下面的操作

  • 圍繞關注節點 a 的父節點 b 左旋;
  • 關注節點 a 的父節點 b 和祖父節點 c 交換顏色;
  • 關注節點不變;
  • 繼續從四種情況中選擇合適的規則來調整。

CASE 2 :如果關注節點是 a,它的兄弟節點 c 是黑色的,並且節點 c 的左右子節點 d、e 都是黑色的,我們就依次執行下面的操作

  • 將關注節點 a 的兄弟節點 c 的顏色變成紅色;
  • 從關注節點 a 中去掉一個黑色,這個時候關注節點 a 就是單純的紅色或者黑色;
  • 給關注節點 a 的父節點 b 新增一個黑色,這個時候節點 b 就變成了 “紅-黑” 或者 “黑-黑”;
  • 關注節點從 a 變成其父節點 b;
  • 繼續從四種情況中選擇合適的規則來調整。

CASE 3 :如果關注節點是 a,它的兄弟節點 c 是黑色的, c 的左子節點 d 是紅色,c 的右子節點 e 是黑色,我們就依次執行下面的操作

  • 圍繞關注節點 a 的兄弟節點 c 右旋;
  • 節點 c 和節點 d 交換顏色;
  • 關注節點不變;
  • 跳到 CASE 4 繼續調整。

CASE 4 :如果關注節點是 a,它的兄弟節點 c 是黑色的,並且 c 的右子節點是紅色的,我們就依次執行下面的操作

  • 圍繞關注節點 a 的父節點 b 左旋;
  • 將關注節點 a 的兄弟節點 c 的顏色,和關注節點 a 的父節點 b 設定成相同的顏色;
  • 將關注節點 a 的父節點 b 的顏色設定成黑色;
  • 從關注節點 a 中去掉一個黑色,節點 a 就變成了單純的紅色或者黑色;
  • 將關注節點 a 的叔叔結點 e 設定成黑色;
  • 調整結束。

4. 紅黑樹為什麼要求葉子節點是黑色的空節點?

之所以有這麼奇怪的要求,其實就是為了實現方便。只要滿足這一條要求,那在任何時刻的平衡操作就都可以歸結為上述的幾種情況。

下面我們來看一個葉子節點如果不為黑色的情況。

當插入一個紅色節點時,紅黑樹的定義就被破壞了,而這個時候這種情況也不滿足上述的三種情況。但如果我們加上黑色的空節點後,它就滿足 CASE 2 了。另外,我們也可以對每種情況的條件進行修改,但那樣的話規則就沒有原來那麼簡潔了。

另外,我們並不是給每個黑色的空的葉子節點都分配一塊記憶體,而是共用一個就行,這樣也不會導致儲存空間的極大浪費。

5. 小結

  • 紅黑樹的確非常複雜,但只要對照著上述這幾種情況,按照固定操作實現即可,不要過分糾結於演算法本身的正確性;
  • 找準關注節點,一切操作都是以關注節點來操作的;

參考資料-極客時間專欄《資料結構與演算法之美》

獲取更多精彩,請關注「seniusen」!