紅黑樹的基本操作之添加
R-B Tree,全稱是Red-Black Tree,又稱爲“紅黑樹”,它一種特殊的二叉查找樹。紅黑樹的每個節點上都有存儲位表示節點的顏色,可以是紅(Red)或黑(Black)。
紅黑樹的特性:
(1)每個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每個葉子節點(NIL)是黑色。 [注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!]
(4)如果一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的葉子節點的所有路徑上包含相同數目的黑節點。
注意:
(01) 特性(3)中的葉子節點,是隻爲空(NIL或null)的節點。
(02) 特性(5),確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹。
將一個節點插入到紅黑樹中,需要執行哪些步驟呢?首先,將紅黑樹當作一顆二叉查找樹,將節點插入;然後,將節點着色爲紅色;最後,通過旋轉和重新着色等方法來修正該樹,使之重新成爲一顆紅黑樹。詳細描述如下:
第一步: 將紅黑樹當作一顆二叉查找樹,將節點插入。
紅黑樹本身就是一顆二叉查找樹,將節點插入後,該樹仍然是一顆二叉查找樹。也就意味着,樹的鍵值仍然是有序的。此外,無論是左旋還是右旋,若旋轉之前這棵樹是二叉查找樹,旋轉之後它一定還是二叉查找樹。這也就意味着,任何的旋轉和重新着色操作,都不會改變它仍然是一顆二叉查找樹的事實。
好吧?那接下來,我們就來想方設法的旋轉以及重新着色,使這顆樹重新成爲紅黑樹!
第二步:將插入的節點着色爲"紅色"。
爲什麼着色成紅色,而不是黑色呢?爲什麼呢?在回答之前,我們需要重新溫習一下紅黑樹的特性:
(1) 每個節點或者是黑色,或者是紅色。
(2) 根節點是黑色。
(3) 每個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點!]
(4) 如果一個節點是紅色的,則它的子節點必須是黑色的。
(5) 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
將插入的節點着色爲紅色,不會違背"特性(5)"!少違背一條特性,就意味着我們需要處理的情況越少。接下來,就要努力的讓這棵樹滿足其它性質即可;滿足了的話,它就又是一顆紅黑樹了。o(∩∩)o...哈哈
第三步: 通過一系列的旋轉或着色等操作,使之重新成爲一顆紅黑樹。
第二步中,將插入節點着色爲"紅色"之後,不會違背"特性(5)"。那它到底會違背哪些特性呢?
對於"特性(1)",顯然不會違背了。因爲我們已經將它塗成紅色了。
對於"特性(2)",顯然也不會違背。在第一步中,我們是將紅黑樹當作二叉查找樹,然後執行的插入操作。而根據二叉查找數的特點,插入操作不會改變根節點。所以,根節點仍然是黑色。
對於"特性(3)",顯然不會違背了。這裏的葉子節點是指的空葉子節點,插入非空節點並不會對它們造成影響。
對於"特性(4)",是有可能違背的!
那接下來,想辦法使之"滿足特性(4)",就可以將樹重新構造成紅黑樹了。
下面看看代碼到底是怎樣實現這三步的。
添加操作的僞代碼《算法導論》
RB-INSERT(T, z)
01 y ← nil[T] // 新建節點“y”,將y設爲空節點。
02 x ← root[T] // 設“紅黑樹T”的根節點爲“x”
03 while x ≠ nil[T] // 找出要插入的節點“z”在二叉樹T中的位置“y”
04 do y ← x
05 if key[z] < key[x]
06 then x ← left[x]
07 else x ← right[x]
08 p[z] ← y // 設置 “z的父親” 爲 “y”
09 if y = nil[T]
10 then root[T] ← z // 情況1:若y是空節點,則將z設爲根
11 else if key[z] < key[y]
12 then left[y] ← z // 情況2:若“z所包含的值” < “y所包含的值”,則將z設爲“y的左孩子”
13 else right[y] ← z // 情況3:(“z所包含的值” >= “y所包含的值”)將z設爲“y的右孩子”
14 left[z] ← nil[T] // z的左孩子設爲空
15 right[z] ← nil[T] // z的右孩子設爲空。至此,已經完成將“節點z插入到二叉樹”中了。
16 color[z] ← RED // 將z着色爲“紅色”
17 RB-INSERT-FIXUP(T, z) // 通過RB-INSERT-FIXUP對紅黑樹的節點進行顏色修改以及旋轉,讓樹T仍然是一顆紅黑樹
結合僞代碼以及爲代碼上面的說明,先理解RB-INSERT。理解了RB-INSERT之後,我們接着對 RB-INSERT-FIXUP的僞代碼進行說明。
添加修正操作的僞代碼《算法導論》
RB-INSERT-FIXUP(T, z)
01 while color[p[z]] = RED // 若“當前節點(z)的父節點是紅色”,則進行以下處理。
02 do if p[z] = left[p[p[z]]] // 若“z的父節點”是“z的祖父節點的左孩子”,則進行以下處理。
03 then y ← right[p[p[z]]] // 將y設置爲“z的叔叔節點(z的祖父節點的右孩子)”
04 if color[y] = RED // Case 1條件:叔叔是紅色
05 then color[p[z]] ← BLACK ▹ Case 1 // (01) 將“父節點”設爲黑色。
06 color[y] ← BLACK ▹ Case 1 // (02) 將“叔叔節點”設爲黑色。
07 color[p[p[z]]] ← RED ▹ Case 1 // (03) 將“祖父節點”設爲“紅色”。
08 z ← p[p[z]] ▹ Case 1 // (04) 將“祖父節點”設爲“當前節點”(紅色節點)
09 else if z = right[p[z]] // Case 2條件:叔叔是黑色,且當前節點是右孩子
10 then z ← p[z] ▹ Case 2 // (01) 將“父節點”作爲“新的當前節點”。
11 LEFT-ROTATE(T, z) ▹ Case 2 // (02) 以“新的當前節點”爲支點進行左旋。
12 color[p[z]] ← BLACK ▹ Case 3 // Case 3條件:叔叔是黑色,且當前節點是左孩子。(01) 將“父節點”設爲“黑色”。
13 color[p[p[z]]] ← RED ▹ Case 3 // (02) 將“祖父節點”設爲“紅色”。
14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 // (03) 以“祖父節點”爲支點進行右旋。
15 else (same as then clause with "right" and "left" exchanged) // 若“z的父節點”是“z的祖父節點的右孩子”,將上面的操作中“right”和“left”交換位置,然後依次執行。
16 color[root[T]] ← BLACK
根據被插入節點的父節點的情況,可以將"當節點z被着色爲紅色節點,並插入二叉樹"劃分爲三種情況來處理。
① 情況說明:被插入的節點是根節點。
處理方法:直接把此節點塗爲黑色。
② 情況說明:被插入的節點的父節點是黑色。
處理方法:什麼也不需要做。節點被插入後,仍然是紅黑樹。
③ 情況說明:被插入的節點的父節點是紅色。
處理方法:那麼,該情況與紅黑樹的“特性(5)”相沖突。這種情況下,被插入節點是一定存在非空祖父節點的;進一步的講,被插入節點也一定存在叔叔節點(即使叔叔節點爲空,我們也視之爲存在,空節點本身就是黑色節點)。理解這點之後,我們依據"叔叔節點的情況",將這種情況進一步劃分爲3種情況(Case)。
現象說明 | 處理策略 | |
Case 1 | 當前節點的父節點是紅色,且當前節點的祖父節點的另一個子節點(叔叔節點)也是紅色。 |
(01) 將“父節點”設爲黑色。 |
Case 2 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子 |
(01) 將“父節點”作爲“新的當前節點”。 |
Case 3 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子 |
(01) 將“父節點”設爲“黑色”。 |
上面三種情況(Case)處理問題的核心思路都是:將紅色的節點移到根節點;然後,將根節點設爲黑色。下面對它們詳細進行介紹。
1. (Case 1)叔叔是紅色
1.1 現象說明
當前節點(即,被插入節點)的父節點是紅色,且當前節點的祖父節點的另一個子節點(叔叔節點)也是紅色。
1.2 處理策略
(01) 將“父節點”設爲黑色。
(02) 將“叔叔節點”設爲黑色。
(03) 將“祖父節點”設爲“紅色”。
(04) 將“祖父節點”設爲“當前節點”(紅色節點);即,之後繼續對“當前節點”進行操作。
下面談談爲什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
“當前節點”和“父節點”都是紅色,違背“特性(4)”。所以,將“父節點”設置“黑色”以解決這個問題。
但是,將“父節點”由“紅色”變成“黑色”之後,違背了“特性(5)”:因爲,包含“父節點”的分支的黑色節點的總數增加了1。 解決這個問題的辦法是:將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”。關於這裏,說明幾點:第一,爲什麼“祖父節點”之前是黑色?這個應該很容易想明白,因爲在變換操作之前,該樹是紅黑樹,“父節點”是紅色,那麼“祖父節點”一定是黑色。 第二,爲什麼將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”;能解決“包含‘父節點’的分支的黑色節點的總數增加了1”的問題。這個道理也很簡單。“包含‘父節點’的分支的黑色節點的總數增加了1” 同時也意味着 “包含‘祖父節點’的分支的黑色節點的總數增加了1”,既然這樣,我們通過將“祖父節點”由“黑色”變成“紅色”以解決“包含‘祖父節點’的分支的黑色節點的總數增加了1”的問題; 但是,這樣處理之後又會引起另一個問題“包含‘叔叔’節點的分支的黑色節點的總數減少了1”,現在我們已知“叔叔節點”是“紅色”,將“叔叔節點”設爲“黑色”就能解決這個問題。 所以,將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”;就解決了該問題。
按照上面的步驟處理之後:當前節點、父節點、叔叔節點之間都不會違背紅黑樹特性,但祖父節點卻不一定。若此時,祖父節點是根節點,直接將祖父節點設爲“黑色”,那就完全解決這個問題了;若祖父節點不是根節點,那我們需要將“祖父節點”設爲“新的當前節點”,接着對“新的當前節點”進行分析。
1.3 示意圖
2. (Case 2)叔叔是黑色,且當前節點是右孩子
2.1 現象說明
當前節點(即,被插入節點)的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子
2.2 處理策略
(01) 將“父節點”作爲“新的當前節點”。
(02) 以“新的當前節點”爲支點進行左旋。
下面談談爲什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
首先,將“父節點”作爲“新的當前節點”;接着,以“新的當前節點”爲支點進行左旋。 爲了便於理解,我們先說明第(02)步,再說明第(01)步;爲了便於說明,我們設置“父節點”的代號爲F(Father),“當前節點”的代號爲S(Son)。
爲什麼要“以F爲支點進行左旋”呢?根據已知條件可知:S是F的右孩子。而之前我們說過,我們處理紅黑樹的核心思想:將紅色的節點移到根節點;然後,將根節點設爲黑色。既然是“將紅色的節點移到根節點”,那就是說要不斷的將破壞紅黑樹特性的紅色節點上移(即向根方向移動)。 而S又是一個右孩子,因此,我們可以通過“左旋”來將S上移!
按照上面的步驟(以F爲支點進行左旋)處理之後:若S變成了根節點,那麼直接將其設爲“黑色”,就完全解決問題了;若S不是根節點,那我們需要執行步驟(01),即“將F設爲‘新的當前節點’”。那爲什麼不繼續以S爲新的當前節點繼續處理,而需要以F爲新的當前節點來進行處理呢?這是因爲“左旋”之後,F變成了S的“子節點”,即S變成了F的父節點;而我們處理問題的時候,需要從下至上(由葉到根)方向進行處理;也就是說,必須先解決“孩子”的問題,再解決“父親”的問題;所以,我們執行步驟(01):將“父節點”作爲“新的當前節點”。
2.2 示意圖
3. (Case 3)叔叔是黑色,且當前節點是左孩子
3.1 現象說明
當前節點(即,被插入節點)的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子
3.2 處理策略
(01) 將“父節點”設爲“黑色”。
(02) 將“祖父節點”設爲“紅色”。
(03) 以“祖父節點”爲支點進行右旋。
下面談談爲什麼要這樣處理。(建議理解的時候,通過下面的圖進行對比)
爲了便於說明,我們設置“當前節點”爲S(Original Son),“兄弟節點”爲B(Brother),“叔叔節點”爲U(Uncle),“父節點”爲F(Father),祖父節點爲G(Grand-Father)。
S和F都是紅色,違背了紅黑樹的“特性(4)”,我們可以將F由“紅色”變爲“黑色”,就解決了“違背‘特性(4)’”的問題;但卻引起了其它問題:違背特性(5),因爲將F由紅色改爲黑色之後,所有經過F的分支的黑色節點的個數增加了1。那我們如何解決“所有經過F的分支的黑色節點的個數增加了1”的問題呢? 我們可以通過“將G由黑色變成紅色”,同時“以G爲支點進行右旋”來解決。
2.3 示意圖
提示:上面的進行Case 3處理之後,再將節點"120"當作當前節點,就變成了Case 2的情況。