紅黑樹下——紅黑樹的實現
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」!