紅黑樹-python版本
簡介
紅黑樹(Red Black Tree) 是一種自平衡二叉查找樹,是在計算機科學中用到的一種數據結構,典型的用途是實現關聯數組。 -百度百科
特性
- 每個節點或者是黑色,或者是紅色。
- 根節點是黑色。
- 每個葉子節點(NIL)是黑色。 [注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!]
- 如果一個節點是紅色的,則它的子節點必須是黑色的。[從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。]
- 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。[簡稱黑高]
注意:
- 特性(3)中的葉子節點,是隻爲空(NIL或null)的節點。
- 特性(5),確保沒有一條路徑會比其他路徑長出倆倍。因而,紅黑樹是相對是接近平衡的二叉樹。
有了上面的幾個性質作爲限制,即可避免二叉查找樹退化成單鏈表的情況。但是,僅僅避免這種情況還不夠,這裏還要考慮某個節點到其每個葉子節點路徑長度的問題。如果某些路徑長度過長,那麼,在對這些路徑上的及誒單進行增刪查操作時,效率也會大大降低。這個時候性質4和性質5用途就凸顯了,有了這兩個性質作爲約束,即可保證任意節點到其每個葉子節點路徑最長不會超過最短路徑的2倍。
特性
-
每個紅黑樹的高度:
-
每個紅黑樹的黑色節點數:
-
增加、刪除和查找的時間複雜度總爲$O(log(n)) $
-
任何一個節點到葉節點的黑色節點數(Black Height):
-
根節點到葉節點最多有個黑色節點
紅黑樹的應用
紅黑樹的應用比較廣泛,主要是用它來存儲有序的數據,它的時間複雜度是,效率非常之高。
用於C++實現Map/Set, Java實現TreeMap/HashMap等數據結構,此外還可以用於實時計算程序和函數式編程中。
紅黑樹的基本操作
樹中的每個節點包括5個屬性:color、key、left、right、parent,如果一個節點沒有子節點或父節點,則該節點的相應指針屬性的值爲NIL
#定義紅黑樹
class RBTree(object):
def __init__(self):
self.nil = RBTreeNode(0)
self.root = self.nil
class RBTreeNode(object):
def __init__(self, x):
self.key = x
self.left = None
self.right = None
self.parent = None
self.color = 'black'
self.size=None
紅黑樹的基本操作是添加、刪除。在對紅黑樹進行添加或刪除之後,都會用到旋轉方法。爲什麼呢?道理很簡單,添加或刪除紅黑樹中的節點之後,紅黑樹就發生了變化,可能不滿足紅黑樹的5條性質,也就不再是一顆紅黑樹了,而是一顆普通的樹。而通過旋轉,可以使這顆樹重新成爲紅黑樹。簡單點說,旋轉的目的是讓樹保持紅黑樹的特性。
旋轉包括兩種:左旋 和 右旋。下面分別對它們進行介紹。
左旋
對x進行左旋,意味着"將x變成一個左節點"。
# 左旋轉
def LeftRotate(T, x):
y = x.right
x.right = y.left # β指向x右側
if y.left != T.nil:
y.left.parent = x #β的父節點指向x
y.parent = x.parent # y父節點更爲爲遠x節點
if x.parent == T.nil:
T.root = y # 考慮x父節點爲根節點情況
elif x == x.parent.left:
x.parent.left = y # x爲父節點的左側值
else:
x.parent.right = y # x爲父節點的右側值
y.left = x # y的右節點指向x
x.parent = y #x的父節點指向y
理解左旋之後,看看下面一個更鮮明的例子。你可以先不看右邊的結果,自己嘗試一下。
右旋
與左旋相對應,將Y進行右旋,意味着"將Y變成一個右節點"
# 右旋轉
def RightRotate(T, y):
x = y.left
y.left = x.right # x的β賦值給y
if x.right != T.nil:
x.right.parent = y # 將β的父節點指向y
x.parent = y.parent # y的父節點給x的父節點
if y.parent == T.nil:
T.root = x # 考慮父節點爲空的情況
elif y == y.parent.right:
y.parent.right = x # y爲父節點的右側值
else:
y.parent.left = x # y爲父節點的左側值
x.right = y # x的右側值指向y
y.parent = x # y的父節點指向x
旋轉總結
(01) 左旋 和 右旋 是相對的兩個概念,原理類似。理解一個也就理解了另一個。
紅黑樹結點操作
添加
將一個節點插入到紅黑樹中,需要執行哪些步驟呢?首先,將紅黑樹當作一顆二叉查找樹,將節點插入;然後,將節點着色爲紅色;最後,通過旋轉和重新着色等方法來修正該樹,使之重新成爲一顆紅黑樹。詳細描述如下:
第一步: 將紅黑樹當作一顆二叉查找樹,將節點插入。
紅黑樹本身就是一顆二叉查找樹,將節點插入後,該樹仍然是一顆二叉查找樹。也就意味着,樹的鍵值仍然是有序的。此外,無論是左旋還是右旋,若旋轉之前這棵樹是二叉查找樹,旋轉之後它一定還是二叉查找樹。這也就意味着,任何的旋轉和重新着色操作,都不會改變它仍然是一顆二叉查找樹的事實。
好吧?那接下來,我們就來想方設法的旋轉以及重新着色,使這顆樹重新成爲紅黑樹!
第二步:將插入的節點着色爲"紅色"。
爲什麼着色成紅色,而不是黑色呢?爲什麼呢?在回答之前,我們需要重新溫習一下紅黑樹的特性:
(1) 每個節點或者是黑色,或者是紅色。
(2) 根節點是黑色。
(3) 每個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點!]
(4) 如果一個節點是紅色的,則它的子節點必須是黑色的。
(5) 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
將插入的節點着色爲紅色,不會違背"特性(5)"!少違背一條特性,就意味着我們需要處理的情況越少。接下來,就要努力的讓這棵樹滿足其它性質即可;滿足了的話,它就又是一顆紅黑樹了。
第三步: 通過一系列的旋轉或着色等操作,使之重新成爲一顆紅黑樹
第二步中,將插入節點着色爲"紅色"之後,不會違背"特性(5)"。那它到底會違背哪些特性呢?
- 對於"特性(1)",顯然不會違背了。因爲我們已經將它塗成紅色了。
- 對於"特性(2)",顯然也不會違背。在第一步中,我們是將紅黑樹當作二叉查找樹,然後執行的插入操作。而根據二叉查找數的特點,插入操作不會改變根節點。所以,根節點仍然是黑色。
- 對於"特性(3)",顯然不會違背了。這裏的葉子節點是指的空葉子節點,插入非空節點並不會對它們造成影響。
- 對於"特性(4)",是有可能違背的!
那接下來,想辦法使之"滿足特性(4)",就可以將樹重新構造成紅黑樹了。
# 紅黑樹的插入
def RBInsert(T, z):
y = T.nil # 新建結點y,y設爲空節點
x = T.root # 設“紅黑色T”的根節點爲 x
while x != T.nil: # 找出要插入結點的z在二叉樹T的位置y
y = x
if z.key < x.key:
x = x.left
else:
x = x.right
z.parent = y # 插入結點z的父節點設爲y
if y == T.nil:
T.root = z # 情況一 y爲空節點,則插入結點設爲根
elif z.key < y.key:
y.left = z # 情況二 若插入值小於父節點值,放在左側
else:
y.right = z # 情況三 若插入值大於父節點值,放在右側
z.left = T.nil # 插入值的左右結點爲空
z.right = T.nil
z.color = 'red' # 默認插入爲紅色
# 對紅黑樹結點進行顏色修改和旋轉,保持紅黑樹性質
RBInsertFixup(T, z)
return z.key, '顏色爲', z.color
修改紅黑樹顏色與旋轉
# 紅黑樹的上色
def RBInsertFixup(T, z):
while z.parent.color == 'red': # 當前父節點爲紅色, 則進行處理,直到符合條件停止
### z的父節點是祖結點的左孩子
if z.parent == z.parent.parent.left:
y = z.parent.parent.right # 將y設置爲z的祖結點的右孩子(叔叔結點)
if y.color == 'red': # 情況一 :叔叔結點爲紅色
z.parent.color = 'black' # 父節點設爲黑色
y.color = 'black' # 叔叔節點設爲黑色
z.parent.parent.color = 'red' # 祖父設爲紅色
z = z.parent.parent # 祖父結點設爲當前結點 ??考慮後續操作
else: # 叔叔結點爲黑色
if z == z.parent.right: # 情況二 :且當前結點是右孩子
z = z.parent # 父節點當做新的當前結點
LeftRotate(T, z) # 當前結點爲支點進行左旋轉
z.parent.color = 'black' # 情況三 :且當前結點是左孩子,將父節點設爲黑色
z.parent.parent.color = 'red' # 祖父結點爲紅色
RightRotate(T, z.parent.parent) # 當前結點爲支點進行右旋轉
### z的父節點是祖結點的右孩子,將上面right和left交換位置,依次執行
else:
y = z.parent.parent.left
if y.color == 'red':
z.parent.color = 'black'
y.color = 'black'
z.parent.parent.color = 'red'
z = z.parent.parent
else:
if z == z.parent.left:
z = z.parent
RightRotate(T, z)
z.parent.color = 'black'
z.parent.parent.color = 'red'
LeftRotate(T, z.parent.parent)
T.root.color = 'black'
根據被插入節點的父節點的情況,可以將"當節點z被着色爲紅色節點,並插入二叉樹"劃分爲三種情況來處理。
① 情況說明:被插入的節點是根節點。
處理方法:直接把此節點塗爲黑色。
② 情況說明:被插入的節點的父節點是黑色。
處理方法:什麼也不需要做。節點被插入後,仍然是紅黑樹。
③ 情況說明:被插入的節點的父節點是紅色。
處理方法:那麼,該情況與紅黑樹的“特性(5)”相沖突。這種情況下,被插入節點是一定存在非空祖父節點的;進一步的講,被插入節點也一定存在叔叔節點(即使叔叔節點爲空,我們也視之爲存在,空節點本身就是黑色節點)。理解這點之後,我們依據"叔叔節點的情況",將這種情況進一步劃分爲3種情況(Case)。
現象說明 | 處理策略 | |
---|---|---|
Case 1 | 當前節點的父節點是紅色,且當前節點的祖父節點的另一個子節點(叔叔節點)也是紅色。 | (01) 將“父節點”設爲黑色。 (02) 將“叔叔節點”設爲黑色。 (03) 將“祖父節點”設爲“紅色”。 (04) 將“祖父節點”設爲“當前節點”(紅色節點);即,之後繼續對“當前節點”進行操作。 |
Case 2 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子 | (01) 將“父節點”作爲“新的當前節點”。 (02) 以“新的當前節點”爲支點進行左旋。 |
Case 3 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子 | (01) 將“父節點”設爲“黑色”。 (02) 將“祖父節點”設爲“紅色”。 (03) 以“祖父節點”爲支點進行右旋。 |
上面三種情況(Case)處理問題的核心思路都是:將紅色的節點移到根節點;然後,將根節點設爲黑色。下面對它們詳細進行介紹。
Case 1 叔叔是紅色
爲什麼要這樣處理
“當前節點”和“父節點”都是紅色,違背“特性(4)”。所以,將“父節點”設置“黑色”以解決這個問題。
但是,將“父節點”由“紅色”變成“黑色”之後,違背了“特性(5)”:因爲,包含“父節點”的分支的黑色節點的總數增加了1。
解決這個問題的辦法是:
將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”。
第一,爲什麼“祖父節點”之前是黑色?這個應該很容易想明白,因爲在變換操作之前,該樹是紅黑樹,“父節點”是紅色,那麼“祖父節點”一定是黑色。
第二,爲什麼將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”;能解決“包含‘父節點’的分支的黑色節點的總數增加了1”的問題。
“第二條原因:包含‘父節點’的分支的黑色節點的總數增加了1” 同時也意味着 “包含‘祖父節點’的分支的黑色節點的總數增加了1”,既然這樣,我們通過將“祖父節點”由“黑色”變成“紅色”以解決“包含‘祖父節點’的分支的黑色節點的總數增加了1”的問題; 但是,這樣處理之後又會引起另一個問題“包含‘叔叔’節點的分支的黑色節點的總數減少了1”,現在我們已知“叔叔節點”是“紅色”,將“叔叔節點”設爲“黑色”就能解決這個問題。 所以,將“祖父節點”由“黑色”變成紅色,同時,將“叔叔節點”由“紅色”變成“黑色”;就解決了該問題。
按照上面的步驟處理之後:當前節點、父節點、叔叔節點之間都不會違背紅黑樹特性,但祖父節點卻不一定。若此時,祖父節點是根節點,直接將祖父節點設爲“黑色”,那就完全解決這個問題了;若祖父節點不是根節點,那我們需要將“祖父節點”設爲“新的當前節點”,接着對“新的當前節點”進行分析。
Case 2 叔叔是黑色,且當前節點是右孩子
爲什麼要這樣處理
首先,將“父節點”作爲“新的當前節點”;接着,以“新的當前節點”爲支點進行左旋。 爲了便於理解,我們先說明第(02)步,再說明第(01)步;爲了便於說明,我們設置“父節點”的代號爲F(Father),“當前節點”的代號爲S(Son)。
爲什麼要“以F爲支點進行左旋”呢?
根據已知條件可知:S是F的右孩子。而之前我們說過,我們處理紅黑樹的核心思想:將紅色的節點移到根節點;然後,將根節點設爲黑色。既然是“將紅色的節點移到根節點”,那就是說要不斷的將破壞紅黑樹特性的紅色節點上移(即向根方向移動)。 而S又是一個右孩子,因此,我們可以通過“左旋”來將S上移!
按照上面的步驟(以F爲支點進行左旋)處理之後:若S變成了根節點,那麼直接將其設爲“黑色”,就完全解決問題了;若S不是根節點,那我們需要執行步驟(01),即“將F設爲‘新的當前節點’”。
那爲什麼不繼續以S爲新的當前節點繼續處理,而需要以F爲新的當前節點來進行處理呢?
這是因爲“左旋”之後,F變成了S的“子節點”,即S變成了F的父節點;而我們處理問題的時候,需要從下至上(由葉到根)方向進行處理;也就是說,必須先解決“孩子”的問題,再解決“父親”的問題;所以,我們執行步驟(01):將“父節點”作爲“新的當前節點”。
Case 3 叔叔是黑色,且當前節點是左孩子
爲什麼要這樣處理
爲了便於說明,我們設置“當前節點”爲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爲支點進行右旋”來解決。
【插入操作口訣】
我根我爸黑,那就很簡單。
我爸要是紅,伯伯也紅,黑爸黑伯紅祖父,
伯伯若黑,三角轉成直,直也轉一次,轉後祖父與兄色互換,全部結束根染黑。
刪除
將紅黑樹內的某一個節點刪除。需要執行的操作依次是:首先,將紅黑樹當作一顆二叉查找樹,將該節點從二叉查找樹中刪除;然後,通過"旋轉和重新着色"等一系列來修正該樹,使之重新成爲一棵紅黑樹。詳細描述如下:
第一步:將紅黑樹當作一顆二叉查找樹,將節點刪除。
這和"刪除常規二叉查找樹中刪除節點的方法是一樣的"。分3種情況:
① 被刪除節點沒有兒子,即爲葉節點。那麼,直接將該節點刪除就OK了。
② 被刪除節點只有一個兒子。那麼,直接刪除該節點,並用該節點的唯一子節點頂替它的位置。
③ 被刪除節點有兩個兒子。那麼,先找出它的後繼節點;然後把“它的後繼節點的內容”複製給“該節點的內容”;之後,刪除“它的後繼節點”。
在這裏,後繼節點相當於替身,在將後繼節點的內容複製給"被刪除節點"之後,再將後繼節點刪除。這樣就巧妙的將問題轉換爲"刪除後繼節點"的情況了,下面就考慮後繼節點。 在"被刪除節點"有兩個非空子節點的情況下,它的後繼節點不可能是雙子非空。既然"的後繼節點"不可能雙子都非空,就意味着"該節點的後繼節點"要麼沒有兒子,要麼只有一個兒子。若沒有兒子,則按"情況① "進行處理;若只有一個兒子,則按"情況② "進行處理。
第二步:通過"旋轉和重新着色"等一系列來修正該樹,使之重新成爲一棵紅黑樹。
因爲"第一步"中刪除節點之後,可能會違背紅黑樹的特性。所以需要通過"旋轉和重新着色"來修正該樹,使之重新成爲一棵紅黑樹。
# 將v替換爲u
def RBTransplant( T, u, v):
if u.parent == T.nil:
T.root = v
elif u == u.parent.left:
u.parent.left = v
else:
u.parent.right = v
v.parent = u.parent
# 獲取當前值下面的最小值
def TreeMinimum( x):
while x.left != T.nil:
x = x.left
return x
# 紅黑樹的刪除
# x保存需要變動子樹的信息,y是在兩側信息都保留情況下 右側最小值
def RBDelete(T, z):
y = z
y_original_color = y.color # 保存節點原先顏色信息
if z.left == T.nil:
x = z.right
RBTransplant(T, z, z.right) # 若左邊孩子爲空 則用右孩子代替(z也可能爲葉子節點)
elif z.right == T.nil:
x = z.left
RBTransplant(T, z, z.left) # 若右邊孩子爲空 則用左孩子代替
else:
y = TreeMinimum(z.right) # 第三種情況,該節點有兩個孩子
y_original_color = y.color # 保存求得最小子節點的顏色信息
x = y.right
if y.parent == z: # 最小值與刪除值z直接相連,則右孩子孩子父節點指向最小值
x.parent = y
else:
# 刪除值z與右側最小值y不直接相連,(主要用最小值的右側替換最小值)
RBTransplant(T, y, y.right)
y.right = z.right
y.right.parent = y
RBTransplant(T, z, y) # 則用y替代z
y.left = z.left
y.left.parent = y
y.color = z.color
if y_original_color == 'black': # 修正顏色,因爲紅色不影響紅黑樹條件特性
RBDeleteFixup(T, x)
修改紅黑樹顏色與旋轉
# 紅黑樹的着色和旋轉
def RBDeleteFixup(T, x):
while x != T.root and x.color == 'black': # 循環終止條件 x不是根或者x的顏色爲紅色
if x == x.parent.left: # 若x是它父節點的左孩子,則 w爲父節點的右孩子(兄弟節點)
w = x.parent.right
if w.color == 'red':
# Case 1: x的兄弟節點顏色爲紅色(此時x的父節點和x的兄弟節點的子節點都是黑節點)
w.color = 'black' # (01) 將x的兄弟節點設爲“黑色”。
x.parent.color = 'red' # (02) 將x的父節點設爲“紅色”。
LeftRotate(T, x.parent)# (03) 對x的父節點進行左旋。
w = x.parent.right # (04) 左旋後,重新設置x的兄弟節點。
if w.left.color == 'black' and w.right.color == 'black':
# Case 2: x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。
w.color = 'red' # (01) 將x的兄弟節點設爲“紅色”。
x = x.parent # (02) 設置“x的父節點”爲“新的x節點”。
else:
# Case 3: x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。
if w.right.color == 'black':
w.left.color = 'black' # (01) 將x兄弟節點的左孩子設爲“黑色”。
w.color = 'red' # (02) 將x兄弟節點設爲“紅色”。
RightRotate(T, w) # (03) 對x的兄弟節點進行右旋。
w = x.parent.right # (04) 右旋後,重新設置x的兄弟節點。
# Case 4: x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的
w.color = x.parent.color # (01) 將x父節點顏色 賦值給 x的兄弟節點。
x.parent.color = 'black' # (02) 將x父節點設爲“黑色”。
w.right.color = 'black' # (03) 將x兄弟節點的右子節設爲“黑色”。
LeftRotate(T, x.parent) # (04) 對x的父節點進行左旋。
x = T.root # (05) 設置“x”爲“根節點”。
else:
# x是它父節點的右孩子,將上面的操作中“right”和“left”交換位置,然後依次執行
w = x.parent.left
if w.color == 'red':
w.color = 'black'
x.parent.color = 'red'
RightRotate(T, x.parent)
w = x.parent.left
if w.right.color == 'black' and w.left.color == 'black':
w.color = 'red'
x = x.parent
else:
if w.left.color == 'black':
w.right.color = 'black'
w.color = 'red'
LeftRotate(T, w)
w = x.parent.left
w.color = x.parent.color
x.parent.color = 'black'
w.left.color = 'black'
RightRotate(T, x.parent)
x = T.root
x.color = 'black'
前面我們將"刪除紅黑樹中的節點"大致分爲兩步
在第一步中"將紅黑樹當作一顆二叉查找樹,將節點刪除"後,可能違反"特性(2)、(4)、(5)"三個特性。
第二步需要解決上面的三個問題,進而保持紅黑樹的全部特性。
爲了便於分析,我們假設"x包含一個額外的黑色"(x原本的顏色還存在),這樣就不會違反"特性(5)"。爲什麼呢?
通過RB-DELETE算法,我們知道:刪除節點y之後,x佔據了原來節點y的位置。 既然刪除y(y是黑色),意味着減少一個黑色節點;那麼,再在該位置上增加一個黑色即可。這樣,當我們假設"x包含一個額外的黑色",就正好彌補了"刪除y所丟失的黑色節點",也就不會違反"特性(5)"。 因此,假設"x包含一個額外的黑色"(x原本的顏色還存在),這樣就不會違反"特性(5)"。
現在,x不僅包含它原本的顏色屬性,x還包含一個額外的黑色。即x的顏色屬性是"紅+黑"或"黑+黑",它違反了"特性(1)"。
現在,我們面臨的問題,由解決"違反了特性(2)、(4)、(5)三個特性"轉換成了"解決違反特性(1)、(2)、(4)三個特性"。
RBDeleteFixup需要做的就是通過算法恢復紅黑樹的特性(1)、(2)、(4)。
RBDeleteFixup的思想是:將x所包含的額外的黑色不斷沿樹上移(向根方向移動),直到出現下面的姿態:
a) x指向一個"紅+黑"節點。此時,將x設爲一個"黑"節點即可。
b) x指向根。此時,將x設爲一個"黑"節點即可。
c) 非前面兩種姿態。
將上面的姿態,可以概括爲3種情況。
① 情況說明:x是“紅+黑”節點。
處理方法:直接把x設爲黑色,結束。此時紅黑樹性質全部恢復。
② 情況說明:x是“黑+黑”節點,且x是根。
處理方法:什麼都不做,結束。此時紅黑樹性質全部恢復。
③ 情況說明:x是“黑+黑”節點,且x不是根。
處理方法:這種情況又可以劃分爲4種子情況。這4種子情況如下表所示:
現象說明 | 處理策略 | |
---|---|---|
Case 1 | x是"黑+黑"節點,x的兄弟節點是紅色。(此時x的父節點和x的兄弟節點的子節點都是黑節點)。 | (01) 將x的兄弟節點設爲“黑色”。 (02) 將x的父節點設爲“紅色”。 (03) 對x的父節點進行左旋。 (04) 左旋後,重新設置x的兄弟節點。 |
Case 2 | x是“黑+黑”節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。 | (01) 將x的兄弟節點設爲“紅色”。 (02) 設置“x的父節點”爲“新的x節點”。 |
Case 3 | x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。 | (01) 將x兄弟節點的左孩子設爲“黑色”。 (02) 將x兄弟節點設爲“紅色”。 (03) 對x的兄弟節點進行右旋。 (04) 右旋後,重新設置x的兄弟節點。 |
Case 4 | x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的,x的兄弟節點的左孩子任意顏色。 | (01) 將x父節點顏色 賦值給 x的兄弟節點。 (02) 將x父節點設爲“黑色”。 (03) 將x兄弟節點的右子節設爲“黑色”。 (04) 對x的父節點進行左旋。 (05) 設置“x”爲“根節點”。 |
Case 1 x的兄弟節點是紅色
(01) 將A的兄弟節點(D)設爲“黑色”。
(02) 將A的父節點(B)設爲“紅色”。
(03) 對x的父節點(B)進行左旋。
(04) 左旋後,重新設置A的兄弟節點。
爲什麼要這樣處理
這樣做的目的是將“Case 1”轉換爲“Case 2”、“Case 3”或“Case 4”,從而進行進一步的處理。對x的父節點進行左旋;左旋後,爲了保持紅黑樹特性,就需要在左旋前“將x的兄弟節點設爲黑色”,同時“將x的父節點設爲紅色”;左旋後,由於x的兄弟節點發生了變化,需要更新x的兄弟節點,從而進行後續處理。
Case 2 x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色
(01) 將x的兄弟節點設爲“紅色”。
(02) 設置“x的父節點”爲“新的x節點”。
爲什麼要這樣處理
這個情況的處理思想:是將“x中多餘的一個黑色屬性上移(往根方向移動)”。 x是“黑+黑”節點,我們將x由“黑+黑”節點 變成 “黑”節點,多餘的一個“黑”屬性移到x的父節點中,即x的父節點多出了一個黑屬性(若x的父節點原先是“黑”,則此時變成了“黑+黑”;若x的父節點原先時“紅”,則此時變成了“紅+黑”)。 此時,需要注意的是:所有經過x的分支中黑節點個數沒變化;但是,所有經過x的兄弟節點的分支中黑色節點的個數增加了1(因爲x的父節點多了一個黑色屬性)!爲了解決這個問題,我們需要將“所有經過x的兄弟節點的分支中黑色節點的個數減1”即可,那麼就可以通過“將x的兄弟節點由黑色變成紅色”來實現。
經過上面的步驟(將x的兄弟節點設爲紅色),多餘的一個顏色屬性(黑色)已經跑到x的父節點中。我們需要將x的父節點設爲“新的x節點”進行處理。若“新的x節點”是“黑+紅”,直接將“新的x節點”設爲黑色,即可完全解決該問題;若“新的x節點”是“黑+黑”,則需要對“新的x節點”進行進一步處理。
Case 3 x的兄弟節點是黑色,x的兄弟節點的左孩子是紅色,右孩子是黑色的
(01) 將x兄弟節點的左孩子設爲“黑色”。
(02) 將x兄弟節點設爲“紅色”。
(03) 對x的兄弟節點進行右旋。
(04) 右旋後,重新設置x的兄弟節點。
爲什麼要這樣處理
我們處理“Case 3”的目的是爲了將“Case 3”進行轉換,轉換成“Case 4”,從而進行進一步的處理。轉換的方式是對x的兄弟節點進行右旋;爲了保證右旋後,它仍然是紅黑樹,就需要在右旋前“將x的兄弟節點的左孩子設爲黑色”,同時“將x的兄弟節點設爲紅色”;右旋後,由於x的兄弟節點發生了變化,需要更新x的兄弟節點,從而進行後續處理
Case 4 x的兄弟節點是黑色,x的兄弟節點的右孩子是紅色的,x的兄弟節點的左孩子任意顏色
(01) 將x父節點顏色 賦值給 x的兄弟節點。
(02) 將x父節點設爲“黑色”。
(03) 將x兄弟節點的右子節設爲“黑色”。
(04) 對x的父節點進行左旋。
(05) 設置“x”爲“根節點”。
爲什麼要這樣處理
我們處理“Case 4”的目的是:去掉x中額外的黑色,將x變成單獨的黑色。處理的方式是“:進行顏色修改,然後對x的父節點進行左旋。下面,我們來分析是如何實現的。
爲了便於說明,我們設置“當前節點”爲S(Original Son),“兄弟節點”爲B(Brother),“兄弟節點的左孩子”爲BLS(Brother’s Left Son),“兄弟節點的右孩子”爲BRS(Brother’s Right Son),“父節點”爲F(Father)。
我們要對F進行左旋。但在左旋前,我們需要調換F和B的顏色,並設置BRS爲黑色。爲什麼需要這裏處理呢?因爲左旋後,F和BLS是父子關係,而我們已知BL是紅色,如果F是紅色,則違背了“特性(4)”;爲了解決這一問題,我們將“F設置爲黑色”。 但是,F設置爲黑色之後,爲了保證滿足“特性(5)”,即爲了保證左旋之後:
第一,“同時經過根節點和S的分支的黑色節點個數不變”。
若滿足“第一”,只需要S丟棄它多餘的顏色即可。因爲S的顏色是“黑+黑”,而左旋後“同時經過根節點和S的分支的黑色節點個數”增加了1;現在,只需將S由“黑+黑”變成單獨的“黑”節點,即可滿足“第一”。
第二,“同時經過根節點和BLS的分支的黑色節點數不變”。
若滿足“第二”,只需要將“F的原始顏色”賦值給B即可。之前,我們已經將“F設置爲黑色”(即,將B的顏色"黑色",賦值給了F)。至此,我們算是調換了F和B的顏色。
第三,“同時經過根節點和BRS的分支的黑色節點數不變”。
在“第二”已經滿足的情況下,若要滿足“第三”,只需要將BRS設置爲“黑色”即可。
經過,上面的處理之後。紅黑樹的特性全部得到的滿足!接着,我們將x設爲根節點,就可以跳出while循環(參考僞代碼);即完成了全部處理。
至此,我們就完成了Case 4的處理。理解Case 4的核心,是瞭解如何“去掉當前節點額外的黑色”。
【刪除操作口訣】
刪除節點兩後代,交換鍵值往下看。
刪除節點是樹根,直接刪除或替換。
刪除節點無後代,自己若黑修雙黑。
刪除節點有一子,若是紅子染成黑。
【修復雙黑操作】
什麼是雙黑和修復雙黑?
雙黑是指刪除操作中替換節點與刪除節點均爲黑色的情況,雙黑標記表明了當前樹違背了黑色節點數目一致的原則,需要進行修復。修復雙黑就是爲了保證紅黑樹滿足上述合法性的操作。
若不存在兄弟: 雙黑要傳遞給父親,對父親進行修復雙黑
兄弟是紅色: 染紅父親,染黑兄弟,把兄弟轉上去,轉化爲兄弟爲黑的情況
兄弟是黑色,此時需要討論兄弟兒子的情況
- 兄弟沒有紅色兒子,先染紅兄弟,然後討論父親顏色的情況
- 父親爲紅色: 染黑父親即可
- 父親爲黑色: 對父親節點修復雙黑
- 兄弟有紅色兒子,根據兄弟與兄弟兒子形成的結構進行分類討論
- 直線型: 兄弟兒子染成黑色,兄弟染成父親色,然後把兄弟轉上去
- 三角型: 把兄弟兒子染成和父親同色,把兄弟兒子轉到兄弟的位置,再把兄弟的兒子轉到父親的位置
參考
https://blog.csdn.net/JERRY_PRI/article/details/8959426
直線型: 兄弟兒子染成黑色,兄弟染成父親色,然後把兄弟轉上去
2. 三角型: 把兄弟兒子染成和父親同色,把兄弟兒子轉到兄弟的位置,再把兄弟的兒子轉到父親的位置
[外鏈圖片轉存中…(img-Cy9nFzZJ-1572491023242)]