1. 程式人生 > >二叉樹到紅黑樹

二叉樹到紅黑樹

二叉樹查詢樹

又叫二叉排序樹。二叉查詢樹或者是一棵空樹,或者是一棵具有如下性質的二叉樹:
	對於任何一個結點X
                若它的左子樹非空,則左子樹上所有結點的值均小於等於X的值;
                若它的右子樹非空,則右子樹上所有結點的值均大於等於X的值;
	按中序遍歷二叉查詢樹,所得到的中序遍歷序列是一個遞增(或遞減)的有序序列。
	我們來說二叉查詢樹當然是和查詢操作有關係,查詢的時間複雜度是和高度H成正比的。也可以這麼說,二叉查詢樹基本操作的時間都是和樹的高度H是成正比的。這些基本操作包括查詢插入和刪除,前驅後繼等。遍歷不在這裡講,以後會專門寫有關遍歷的文章,BFS,DFS,無棧非遞迴等。

查詢

我們先來看一下查詢:給定一個樹的樹根指標x和關鍵字k,返回指向關鍵字的指標,否則返回NIL。 TREE-SEARCH(x,k)虛擬碼


在遞迴查詢的過程中遇到的結點即構成了一條由樹根下降的路徑,故TREE-SEARCH的時間複雜度是O(H)。
再來一個非遞迴版本的ITERATIVE-TREE-SEARCH,這個比遞迴的要快一些。


最大值和最小值

那麼如何去找最大值和最小值呢?最大值肯定是二叉樹的最右結點啦,從根結點開始沿著right指標一路走到right指標為空。而最小值肯定在最左結點,從根結點沿著left指標一路走到left指標為空。(此時說的二叉樹是按中序遍歷是從小到大)


	時間複雜度都是O(H),因為尋找的過程就是根結點一路下降的過程。

前驅和後繼

找到最大值和最小值還不夠,有時候我們需要尋找前驅和後繼。如何尋找呢?我們這裡來講述一下中序遍歷的的前驅和後繼尋找。後序遍歷和前序遍歷的前驅後繼的尋找和這是一樣的道理。

後繼

先說後繼,對於一個結點X來說,後繼無非是有3種情況: 1 X有右孩子,那麼X的後繼就是右孩子的最左結點。 2 X無右孩子,而且X是其父母的左孩子,那麼X後繼就是其父母結點(可能NIL)。 3 X無右孩子,而且X是其父母的右孩子,那麼X的後繼就是X的某個祖先(這個最低祖先的左孩子也是X的祖先),此時讓父母成為新的X,尋找不斷找X是其父母的左孩子的結點,如果此時出現一個X結點是其父母左孩子,那個父母就是X的後繼。這個意思也就是說把第三種情況變成第二種情況。如果這個時候找不到則得到的是NIL結點。因為最大值是沒有後繼的,其他的肯定都有。
對於第一種情況很簡單


對於第二種情況,更簡單了,可能父母是NIL,即X結點是根結點。


對於第三種情況,找不到的話就是NIL。最大值沒有後繼。

虛擬碼

前驅

	再來看一下前驅,前驅和後繼是對稱的。如果你仔細看,你會發現剛才尋找後繼2 、3 情況的過程是X一直向上查詢X是否是其父母的左孩子,而此時X的前驅的分類是和他對稱的,是一直向上尋找看X是否是其父母的右孩子,那麼其父母就是其前驅。
	1  X有左孩子,那麼X的前驅就是左孩子的最右結點。
	2  X無左孩子,而且X是其父母的右孩子,那麼X前驅就是其父母結點(可能NIL)。
	3  X無左孩子,而且X是其父母的左孩子,那麼X的前驅就是X的某個祖先(這個最低祖先的右孩子也是X的祖先),此時讓父母成為新的X,尋找不斷找X是其父母的右孩子的結點,如果此時出現一個X結點是其父母右孩子,那個父母就是X的前驅。這個意思也就是說把第三種情況變成第二種情況。如果這個時候找不到則得到的是NIL結點。最小值沒有前驅。
對應第一種情況

對應於第二種情況,父母可能是NIL。

對應於第三種情況,找不到是NIL。最小值沒有前驅。

虛擬碼
TREE-PREDECESSOR(x)
1 if left[x]≠NIL
2    then return TREE-MAXIMUM(left[x])
3 y←p[x]
4 while y≠NIL and x=left[y]
5      do  x←y
6          y←p[y]
7 return y
	時間複雜度都是O(H),就是一條自結點向上的路徑罷了。
	綜上所述 對於一顆高度為H的二叉查詢樹來說,靜態操作SEARCH,MINIMUN,MAXIMUM,SUCCESSOR,PREDECESSOR等用時都是O(H)。
	對於一顆二叉樹,要的不僅僅是靜態操作,還需要動態操作。因為必然會有插入和刪除操作,而且還要保持二叉排序樹的性質。

插入

先來看如何插入結點Z,插入很簡單的。先把結點Z的左右孩子left[z] 、right[z]和父母p[z]初始化,然後從根結點開始一直向下尋找Z的父母,找到之後就把孩子Z插入,如果是空樹,直接把Z當成根結點。 在虛擬碼中,X初始化為根結點,Y始終是其父母,最後把Z插入。


TREE-INSERT的時間複雜度是O(H)。

刪除

插入結點很簡單,那麼如何刪除結點呢?刪除結點之後還要保持中序遍歷是一個相對位置不變的序列。對於刪除一個結點Z,其實是要分類的。 1 如果Z沒有孩子,直接刪除,僅僅修改其父結點P[Z]的孩子為NIL即可。 2 如果Z僅僅只有一個孩子,那麼刪除Z之前要把Z的孩子賦值給其父母P[Z]的孩子(左|右)。這樣一個拉鍊直接去刪除Z了。 3 如果Z有兩個孩子,這樣就有點麻煩了。因為刪除Z之後還有兩個孩子,如果不好好處理會破壞二叉樹的性質,我們刪除Z一定要保持中序遍歷的相對位置序列不變。這樣有兩種方法,因為只有保持性質即可,兩種方法得來的二叉樹不一定是一樣的,但是都是正確的二叉樹。 我們來看一下第三種情況的兩種方法:假設刪除的結點是P,而且中序遍歷的序列是{....CL ,C.... QL,Q,SL,S,P,PR,F}刪除P之後的中序遍歷序列應該是{....CL ,C.... QL,Q,SL,S,PR,F}。S是P的前驅,PR是P的後繼。
這就是未刪除P之前的二叉樹原型。

方法一

第一種方法是,先把P的前驅S的左孩子SL(S肯定無右孩子,左孩子可能是NIL)變成S父母Q的右孩子,刪除S。然後把S的值全部給P,這就讓P前驅S取代了P的位置,這樣序列相對位置沒有發生變化。
	可是真的是這樣嗎?這是老嚴資料結構書上的截圖,算導上給的是找後繼結點,然後和上邊說的一樣,就是把剛才的前驅換成後繼。
	可是我想說的是這裡忽略了一種情況,那就是如果前驅是其左孩子了怎麼辦?根據之前分析的前驅分類,在這裡要刪除的結點P肯定有兩個孩子,那麼對於前驅來說就只有兩種情況,如果P前驅是左孩子的最右結點,剛才的分析沒有一點問題,可是如果P前驅是其左孩子,也就是說左子樹根結點沒有右子樹。那麼按剛才的分析 就是說不通了。前驅S是P的左孩子,你還要把S的左孩子變成S的父母(P)的右孩子嗎?那之前P的右孩子豈不是丟了??!!
	所以這個時候你要判斷一點就是P前驅是否是P的左孩子,就是在找前驅的時候保留一個指標指向前驅的父母,最後判斷是否和P相等,如果相等就把P左孩子的左孩子變成P的左孩子,然後P的左孩子(此時也是前驅)覆蓋P。如果不相等,就按剛才說的做。
	算導給的,和這是一樣的,只不過是對稱找後繼,如果後繼是其右孩子也是尷尬。也可以和剛才一樣,留個指標判斷。
	我們要分類判斷,如果判斷得到P的前驅是其左孩子的話,那麼直接將前驅取代P就好了,其他什麼都不用做。或者像算導上說的找後繼,後繼是其右孩子的話就讓後繼取代他。也就是說如果一個結點的前驅或者後繼是其孩子的話,直接讓其孩子取代他就好了,很簡單的,其他什麼都不用做。
	不能想當然的以為前驅一定是左孩子的左右結點,後繼一定是右孩子的最左結點。這樣就算是前驅不是其左孩子,只要後繼是其右孩子,就讓右孩子取代他,兩個孩子滿足一個就可以減少很多的處理,優化演算法,而且此時不必留指標再判斷了。如何判斷呢?如果P的左孩子沒有右子樹,那前驅就是左孩子。如果他的右孩子沒有左子樹,那後繼就是其右孩子。
虛擬碼
TREE-DELETE(T,z)
1 if left[z]=NIL or right[z]=NIL
2    then if left[z]=NIL
3            then  p[right[z]]=p[z]
4                  left[p[z]]=right[z]
5         else if right[z]=NIL
6            then p[left[z]]=p[z]
7                 left[p[z]]=left[z]
8 else if  right[left[z]]=NIL
9          then key[z]=key[left[z]]
10              p[left[left[z]]]=z
11              left[z]=left[left[z]]
12 else if left[right[z]]=NIL
13         then p[right[right[z]]]=z
14              right[z]=right[right[z]]
15 else
16      then y=left[z]
17           while right[y]≠NIL
18                 y=right[y]
19           p[left[y]]=p[y]
20           right[p[y]]=left[y]
21           key[z]=key[y]
	這是一種分類的做法,老嚴給的版本是找後繼判斷,算導給的是找前驅判斷,我這裡給他綜合了一下,更加的優化。不過算導版本雖然沒有分類,但是有各種判斷,程式碼很緊湊。雖然我感覺沒我的好,不過還是在這裡貼一下。


方法二

	接下來我們看第二種方法,這種方法的好處是不像剛才那麼有BUG,不過好像大多數書上舉的例子都是第一種,汗。難道第一種比第二種的簡單?我們看一下吧
	先令P的右孩子PR成為其前驅S的右孩子,然後將P的左孩子C成為P父母F的左孩子,刪除P。這樣就不用管C是不是其前驅了。C就是其前驅也是把PR給C的右孩子,序列的相對位置是不會發生變化的。


來看一下虛擬碼
TREE-DELETE(T,z)
1 if  left[z]=NIL or right[z]=NIL
2     then if left[z]=NIL
3             then  p[right[z]]=p[z]
4                   left[p[z]]=right[z]
5          else if right[z]=NIL
6                  then p[left[z]]=p[z]
7                       left[p[z]]=left[z]
8  else 
9       y=TREE-PREDECESSOR(z)
10      p[right[z]]=y
11      right[y]=right[z]
12      p[left[z]]=p[z]
13      left[p[z]]=left[z]
程式碼很簡潔,我反倒覺得這個簡單,還不麻煩。這兩種方法都很簡單的,一時想不起,就拿筆畫一顆二叉樹,很快就明白了。一定要動手畫一下。時間複雜度很明顯是O(H)。 上邊所說的二叉樹的大部分操作的時間複雜度都是O(H),H是樹的高度。大家一聽高度想當然以為就是lgN,其實非也。一個好的二叉樹高度可能是O(lgN),即便是如此,可是經過了很多的插入和刪除,高度就會發生變化。就像是一開始只有一個結點的二叉樹,最後變成了一個高度為N-1的鏈。這雖然還是樹,可是已經不是我們需要的了,時間複雜度太大了(O(N)). 可是我們怎麼才能保證一個樹的高度是lg級的呢?隨機。一個N個關鍵字隨機的構造的二叉樹的高度是lgN。可是經過刪除和再次插入的洗禮,高度也不會一直是lgN。所以我們二叉樹期望高度是O(lgN),平均時間複雜度是良好的,不過還是不可避免的是會出現最壞情況。這個時候我們就不能僅僅依靠隨機了。

平衡二叉樹

不過我們不會任由這種最壞情況的發生,我們可以對二叉樹做一些平衡的處理,接下來就介紹一下平衡二叉樹,這個在最壞情況的時間複雜度都是lg級。 平衡二叉樹是為了避免剛才的最壞情況產生的,平衡是指所有的葉子的高度趨於平衡,更廣義的是指在平衡二叉樹上所有可能查詢的均攤複雜度偏低。幾乎所有平衡樹的操作都基於樹的旋轉操作,通過旋轉操作可以使得樹趨於平衡。AVL樹,紅黑樹,伸展樹,Treap樹等都是平衡二叉樹。

AVL樹

AVL樹僅僅只是平衡二叉樹的一種而已,具有以下性質:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵AVL樹。若二叉樹上結點的平衡因子定義為該結點的左子樹的高度減去右子樹的高度。那麼AVL樹的平衡因子就只能是0,-1,1。如果有一個結點的平衡因子的絕對值大於1 ,那麼這個二叉樹就不是AVL樹。如果由於插入和刪除結點導致AVL樹不平衡,那麼就從這個結點開始進行一次或多次的左右旋轉和高度的調整,最後使二叉樹變得平衡。 老嚴那本書上講的平衡二叉樹僅僅只是AVL演算法的一種平衡二叉樹而已,不代表全部的。注意平衡二叉樹一種平衡樹,而AVL演算法只不過是調整樹的高度保持平衡的演算法而已,不是平衡二叉樹的全部。平衡二叉樹有很多種,這裡我把一些以前的AVL筆記中中的旋轉K過來看一下吧。

紅黑樹

這裡我們主要是說一個特殊的平衡二叉樹,那就是紅黑樹RB-TREE。RB-TREE的好處在於檢索很快,主要用於關聯式容器。Set map multiset multimap的底層實現都是RB-TREE,這些關聯容器存入的時候都是自動排序,查詢的時候都是很快。不過代價就是RB-TREE樹的插入和刪除帶來的麻煩很多,不過還好的是 它可以在O(log n)時間內做查詢,插入和刪除。 紅黑樹在每個結點上增加一個儲存位表示結點的顏色,可以是Red或Black。每一個結點有5個域:color key left right p。如果某個結點沒有孩子或沒有父結點,則把相應的結點設為NIL,顏色BLACK。 紅黑樹是每個結點都帶有顏色屬性的二叉查詢樹,顏色為紅色或黑色。在二叉查詢樹強制一般要求以外,對於任何有效的紅黑樹我們增加了如下的額外要求(性質): 性質1. 每個結點只能是紅色或黑色。 性質2. 根結點是黑色。 性質3. 所有葉子結點都是黑色(葉子是NIL結點)。 性質4. 每個紅色結點的兩個孩子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色結點) 性質5. 從任一結點到其每個葉子的所有簡單路徑都包含相同數目的黑色結點。 通過對任何一條從根到葉子的路徑上各個結點著色方式的限制,紅黑樹確保沒有一條路徑會比其他路徑長出倆倍,因而是接近平衡的。


	在很多樹資料結構的表示中,一個結點有可能只有一個子結點,而葉子結點包含資料。這種表示紅黑樹是可能的,但是這會改變一些屬性並使演算法複雜。為此,我們使用 NIL來表示葉子結點。如上圖所示,它不包含資料而只充當樹在此結束的指示。這些結點在繪圖中經常被省略,導致了這些樹好像同上述原則相矛盾,而實際上不是這樣。與此有關的結論是所有結點都有兩個子結點,儘管其中的一個或兩個可能是空葉子NIL。有點時候為了節省記憶體把所有的NIL連在一起當成一個NIL[T]哨兵,代表所有的葉子NIL和父母結點是NIL的結點。
	結果是這個樹大致上是平衡的。因為操作比如插入、刪除和查詢某個值的最壞情況時間都要求與樹的高度成比例,這個在高度上的理論上限允許紅黑樹在最壞情況下都是高效的,而不同於普通的二叉查詢樹。
	要知道為什麼這些特性確保了這個結果,注意到性質4導致了路徑不能有兩個相連的紅色結點就足夠了。最短的可能路徑都是黑色結點,最長的可能路徑有交替的紅色和黑色結點。還有注意的是葉子節點都是NIL,它不包含資料而只充當樹在此結束的指示。所以根據屬性5所有最長的路徑都有相同數目的黑色結點,這就表明了沒有路徑能多於任何其他路徑的兩倍長。
	我們定義黑高度BH(X)為從結點X出發(不包括X)到葉子結點的任意一條路徑上,黑色結點的個數。這樣我們就會得到一個結論:一顆有N個結點(不包括NIL葉子)的紅黑二叉樹的高度至多是2lg(N+1)。
	簡單證明一下:由於以X為根的RB-TREE至少包含2^BH(X)-1個結點。因為對於高度為0的RB樹,結點數目是0,對於一個結點高度大於0的RB樹,左右孩子的顏色可能是紅,也可能是黑,所以孩子的黑高度BH有可能和父母一樣,也有可能比父母少1。所以孩子的結點數目加上父母結點X就和X為根的RB樹的結點數目是一樣的,也就是說2^(BH(X)-1)-1+2^(BH(X)-1)-1+1=2^BH(X)-1,所以假設成功。此時呢,假設N個結點的RB樹高度是H,則黑高度至少是H/2,所以呢,2^BH-1=2^(H/2)-1<=N,H<=2lg(N+1),得證。
	所以對於紅黑樹的所有動態和靜態的操作都可以在O(lgN)時間內實現。

旋轉

可是經過了刪除和插入,紅黑樹的性質會發生變化,所以需要一些旋轉操作和結點顏色的改變來維持性質。這裡我們先來看一下旋轉的知識。旋轉分左旋和右旋。
	當在某個結點x上做左旋操作時,我們假設它的右孩子y不是NIL,x可以為RB樹內任意右孩子而不是的結點。旋以x到y之間的鏈為“支軸”逆時針旋轉,它使y成為該子樹新的根,而y的左孩子b則成為x的右孩子。而右旋和左旋是對稱的。Y的右孩子X也不是NIL,這時候以y到x之間的鏈為“支軸”逆順時針旋轉,使x成為新的子樹的根,x的右孩子變成y的左孩子。因為左右對稱,這裡我們只給出左旋的虛擬碼。假設X的右孩子不等於NIL,而且根的父母結點是NIL。

	時間複雜度都是O(1),旋轉的時候只有指標在變化,而其他的所有域都不會改變。至於RB樹顏色的變化是和旋轉無關的。還要明確一點的就是旋轉不改變中序遍歷的相對順序

插入

現在就先說插入吧,因為插入畢竟比刪除簡單一些。在一個有N個RB樹中插入一個結點可以在O(lgN)時間內完成。 插入一個結點肯定是要賦予顏色的,我們是給他紅色,還是給他黑色呢? 5大性質: 性質1. 每個結點只能是紅色或黑色。 性質2. 根結點是黑色。 性質3. 所有葉子結點都是黑色(葉子是NIL結點)。 性質4. 每個紅色結點的兩個孩子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色結點) 性質5. 從任一結點到其每個葉子的所有簡單路徑都包含相同數目的黑色結點。 根據5大性質我們知道,如果把插入的結點賦予黑色,則會違反性質5,導致黑高度發生變化,這個是很麻煩的。待會刪除的時候你就會明白。而如果賦予紅色,則只會違反性質2和4,如果是性質2,說明插入的是根結點,直接把根結點變成黑色就好。如果違反性質4,說明插入結點的父節點是紅色的,此時可以通過旋轉和結點顏色的改變搞定,這個不是很麻煩。我們只討論違反性質的情況,不違反的就不說了。我們來看一下。 RB-TREE的插入和之前的二叉查詢樹的插入程式碼有些不同,所有的NIL結點換成哨兵NIL[T],插入的結點初始化left和right都是哨兵NIL[T],顏色直接賦予紅色。然後專門寫一個調整演算法RB-INSERT-FIXUP。這裡我們只看調整演算法,之前的插入我們已經講過了,不明白看前面章節。 我們假設父節點是P,叔叔結點是U,祖父結點是G,插入結點是N。同時假設Z的父節點P是祖父G的左孩子,這和是右孩子是對稱的,我們只討論P是G左孩子的情況。 如何調整呢?我們要尋找一些輔助,父節點P肯定是要參與的,因為要看他是不是紅色,紅色的話就調整,不是紅色的話直接給根結點變黑色(不判斷都行),不管其他的。而且可以肯定是祖父節點G肯定是存在的,而且是黑色(NIL或者其他,因為插入之前肯定滿足RB樹的性質)。如果父結點P是紅色,那麼我們如何調整?因為除了根結點是黑色之外,其他的都不曉得,我們也不能隨意的調整。

情況一

	我們不能貿然調整把插入結點N變成黑色,不過如果父結點的兄弟結點,也是插入結點N的叔叔結點U,他如果是紅色的話。

	此時我們可以把U和P都變成黑色,但是此時以祖父的父節點G為根的子樹的黑高度變化了,也即是+1了。這就違反了性質5,唉,千萬不敢違反性質5,超級麻煩。所以我們做一個變通,可以把祖父結點G變成紅色,此時的違反性質的可能性就落到了G身上,這時就把G當成插入結點N,繼續進入調整演算法開始調整。
	也就是說我們可以把這個插入結點N和父節點P的連續紅色的違反情況向上移動,就是把違反性質的結點向上調整,這樣直到最後變成根結點是紅色的,根結點父節點是NIL[T]黑色,此時直接把根結點變成黑色即可,很nice。


	此時的G就變成了新的插入結點了,繼續調整,最多到根結點。
情況一:如果父節點P和叔父節點U二者都是紅色,則我們可以將它們兩個重繪為黑色並重繪祖父節點G為紅色(用來保持性質5)。
	可是如果叔叔結點U是黑色呢?這就不能按剛才說的調整了,我們要換一個策略了。此時我們要分情況,插入結點N是父節點P的左孩子還是右孩子,這是不一樣的。

情況三

如果N是P的左孩子
	此時我們直接把父節點P變成黑色,然後以G--P為軸順時針旋轉(右旋),P變成了根,但是G變成了P的右孩子,這個時候兩邊的黑高度就發生了變化,右邊的+1了,此時我們可以把G變成紅色,這樣兩邊就均衡了啊。而且
此時演算法直接就結束了,因為性質都保持住了。


情況二

可是如果N是P的右孩子呢?

	這樣看起來很不好搞啊,旋轉變色,都達不到要求,都是無用的。我們可以把這個N右孩子變成剛才說的左孩子嗎?看起來是不可以。不過我們可以發現的是父母和孩子結點的變換是可以通過旋轉來做的,也就是說通過一次旋轉,孩子可以變成父母,父母也可以變成孩子,而新的孩子和之前的孩子的左右是相反的。而此時N和P都是紅色,我們通過一次的左旋,可以把P變成N的左孩子,也就是相當於P變成插入結點是紅色,N變成父節點是紅色,叔叔結點U是黑色,則和剛才說的是一樣的。

	也即是變成了剛才那種情況。所以我們把這種情況稱為第二種,剛才說的是第三種。因為第二種到第三種是直接過度的,不是互斥的情況。雖然我們忘了一種情況,那就是叔叔不存在,但是這並不影響我們剛才分析的結果。
情況二:如果父節點P是紅色而叔父節點U是黑色或不存在,並且插入節點N是其父節點P的右孩子。我們以P-N為軸進行一次左旋調換插入節點N和其父節點P的角色(不是顏色)

情況三::如果父節點P是紅色而叔父節點U是黑色或不存在,並且插入節點N是其父節點P的左孩子。我們可以把P重繪為黑色,並重繪G為紅色。然後以P-G為軸以進行一次左旋調換節點P和節點G的角色。
	情況一要進入繼續調整,最壞的情況是向上調整到根。情況二直接進入情況三。情況三調整完了之後調整演算法就結束了。
	上虛擬碼,這個虛擬碼和剛才分析的是一樣的,如此同時還是隻有半邊程式碼,就是隻有父節點P是祖父結點G的左孩子的情況。不過在虛擬碼裡,插入結點是z,y是其叔叔。
INSERT虛擬碼


RB-INSERT-FIXUP虛擬碼

給一個例子看看,很好的例子,囊括了三種情況。
這是情況一的情況

情況一調整結果變成了情況二了。


情況二調整結果變成了情況三了。

情況三調整完了就結束了。

	很明顯我們的RB-INSERT的1-16行的時間代價是(lgN),而對於RB-INSERT-FIXUP中,只有情況一才會發生迴圈,即便是迴圈到根,也不過用時O(lgN),所以總的時間花費就是O(lgN)。而且這個RB-INSERT-FIXUP過程的旋轉不超過2次,也就是情況二和情況三各旋轉一次O(1),旋轉過後演算法就結束了。

刪除

接下來我們來看看如何刪除RB-TREE的結點。
	對於一個有N個結點的RB-TREE來說,刪除一個結點要花費的時間就是O(lgN)。與插入相比,只不過是更加的複雜罷了。我們來分析一下。刪除一個二叉樹的結點我們說過了,這裡不再說,之說刪除結點之後如何保持紅黑樹的5大性質。
5大性質:
	性質1. 每個結點只能是紅色或黑色。
	性質2. 根結點是黑色。
	性質3. 所有葉子結點都是黑色(葉子是NIL結點)。
	性質4. 每個紅色結點的兩個孩子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色結點)
	性質5. 從任一結點到其每個葉子的所有簡單路徑都包含相同數目的黑色結點。
	如果刪除的結點的顏色是一個紅色,我們壓根就不需要調整,因為這並不違反任何一個性質。因為各結點黑高度沒有變化,不存在相鄰的紅色結點,刪除的也不是根結點。如果樹的結點只有一個,那麼刪除了也不需要調整。如果樹的結點不是一個而且刪除的結點是黑色的,那麼就有可能違反性質4,因為可能有兩個相鄰的紅色結點。刪除了根結點,造成新的根結點是紅色可能違反了性質2。而且刪除黑色的結點會違反性質5,導致黑高度不一致。
	我們如何調整呢?假設刪除了結點Y,肯定有佔用他位置的結點是X,X可能是他的前驅或後繼,X也可能是Y的孩子,或者是哨兵NIL[T]結點。如果X結點本身是紅色,也就是說有可能違反性質4和5,那我們直接賦予它黑色就解決了。如果紅色結點X變成了根違反性質1,也直接賦予黑色就OK。現在的問題是如果X結點是黑色而且不是根結點,違反了性質5,黑高問題就不好解決了。這裡我們主要說如何解決這一個情況,前兩個情況很easy,不需細說了。
	之前說過如果違反了性質5,會很麻煩,這會你就知道了。違反了性質5,我就很拙計了。當然這也不是解決不了的。那如何保持性質5呢?我們總不能期望他不違反性質5吧。這裡我們用一個技巧,就是假設刪除的結點Y把它的黑色性質X結點了,也就是說X結點多了一重黑色,就是黑+黑,這樣的話黑高度就一致了,性質5就滿足了。
	當然這只是假設,我們假想的,不過這有助於我們解決問題。這個黑+黑結點是X結點,他的顏色屬性還是黑色,只不過用一個指標指向他,他便多了一層黑色。不斷調整的過程,會出現新的X結點,這時候指標指向的結點不一定會是原來的X了,不過還是會多一層黑色。而且在不斷旋轉的過程會出現旋轉前後兩邊子樹黑高發生變化的情況,此時如果N這邊的子樹黑高+1了,那N頭上的多於黑色就沒有了,捨棄。這就是我們的最終目的。
	這也就是調整的麻煩之處,接下來我們看看他如何麻煩。當然這還是要分類的。這裡我們只考慮結點X是其父節點左孩子的情況,對稱的情況不再講。
	我們假設佔用被刪除結點的位置的結點是N,N的兄弟結點是S,N的父親結點是P,S的左孩子是SL,S是右孩子是SR。要明確一點的是S不是NIL[T],因為如果是NIL[T],那兩邊的黑高現在就不一樣,即便是沒刪除之前,RB-TREE也不會出現這種情況的。

情況一

如果兄弟結點S是紅色,那麼S的父親結點P和S的孩子SL和SR都必然是黑色,此時N也是黑色的。


	此時如何調整?指標指向的是N,N是黑+黑。此時父親結點P變紅色,S變黑色,然後以P-S為軸左旋。此時指標指向的還是N,看起來好象是沒什麼變化,其實是變了,父親結點P變成了紅色,兄弟結點變成SL還是黑色。不要著急,這只是第一步而已嘛。


情況一:如果兄弟結點S是紅色的(隱含的意思P,SL,SR都是黑色),那麼我們就交換父節點P和兄弟結點S的顏色,即P變紅色,S變黑色,然後以P-S為軸左旋。
如果兄弟結點是紅色的話,只可能有這一種情況,因為父節點和孩子結點的顏色都是定好的黑色。接下來看兄弟結點是黑色的情況,這樣他的孩子結點和父親結點的顏色可就不唯一了,這可以分好幾種情況的。

情況二

如果N的兄弟結點S是黑色的,而且兩個孩子結點都是黑色的。N還是被指標指向,黑+黑。(P的顏色不曉得
	如何調整?此時我們並不知道P的顏色,我們也不需要去判斷。我們假設N和S都脫掉一層的黑色(兩邊路徑上的黑色結點數還是相同),那N變成了正兒八經的黑色了,指標也不指向他了,而S脫掉黑色就只能變成了紅色。而此時經過P的路徑在這邊少了一個黑色,黑高-1,所以我們要用指標指向他,給他多一層的黑色,這樣經過P的路徑在這邊的黑色就不變了,這樣多好。

	此時指標指向的是P(新的N)多了一層黑色,黑+黑或者是紅+黑。有變化嗎?當然了,看起來好像回到了情況一(兄弟S是紅色),其實不是。因為新的N是P,這樣指標指向的結點不斷的向上移動,至多變成了根結點,那敢情好,直接賦予黑色,OK搞定。
情況二:如果兄弟結點S是黑色的,而且S的兩個孩子也都是黑色的。直接把N變成黑色,S變成紅色,用指標指向P,P多了一層黑色。

情況三

如果N的兄弟結點S是黑色,不過S的左孩子SL是紅色的,右孩子SR是黑色的。
	那麼如何調整?此時把SL和S的顏色交換,即SL變成黑色,S變成紅色,然後以S-SL為軸右旋。此時N的兄弟變成了SL,而SL的右孩子變成了紅色。

情況三:如果N的兄弟結點S是黑色,而且S的左孩子SL是紅色的,右孩子SR是黑色的。此時把SL變成黑色,S變成紅色,然後以S-SL為軸右旋。

情況四

此時N的兄弟S的右孩子就變成了紅色,而左孩子不曉得。這種情況就是我們要說的情況四,情況三直接過渡到情況四,相互不排斥,其實是在一個分類裡面。

	這樣的話就是直接交換P和S的顏色,P變成黑色,而S變成P的顏色(不定),S的右孩子SR也變成黑色。然後以P-S為軸左旋。此時只有S的顏色不定,可是此時S的孩子都是黑色,S是什麼色都無所謂了。此時演算法就結束了,調整完畢。

演算法為什麼就結束了呢?因為調整的前後左邊的子樹的黑高+1了,所以此時N的多一層黑色就沒用了,要捨棄。就是說我們解決了黑高的問題,性質5保持住了。
情況四:如果N的兄弟結點S是黑色,而且S的右孩子SR是紅色的。那麼交換P和S的結點顏色,即S變成P的顏色(不定),P變成黑色,SR變成黑色,然後以P-S為軸左旋。
	OK,大功告成一半,接下來上虛擬碼。虛擬碼中Y是要刪除的結點,X是替代Y的結點,W是X的兄弟。調整演算法是RB-DELETE-FIXUP。
RB-DELETE(T,x)
........
if color[y]=black
   then RB-DELETE-FIXUP(T,x)

     RB-DELETE-FIXUP的時間複雜度是O(lgN),因為只有情況二需要迴圈,迴圈次數最多是O(lgN)。情況 1 、3、 4最多需要旋轉3次和一些顏色的改變而已。所以RB-DELETE-FIXUP過程要花費O(lgN)的時間,至多需要3次旋轉。總的來說紅黑樹是很不錯的,很快,所以很多容器的底層都用RB樹。
	OK,這樣紅黑樹就差不多說完了,以後有新的理解還會繼續更新。