紅黑樹的定義

http://wangdei.javaeye.com/blog/236157

 

紅黑樹的定義

正如在CLRS中定義的那樣(譯者: CLRS指的是一本著名的算法書Introduction to Algorithms,中文名應該叫算法導論,CLRS是該書作者Cormen, Leiserson, Rivest and Stein的首字母縮寫),一棵紅黑樹是指一棵滿足下述性質的二叉搜索樹(BST, binary search tree):

1. 每個結點或者爲黑色或者爲紅色。
2. 根結點爲黑色。
3. 每個葉結點(實際上就是NULL指針)都是黑色的。
4. 如果一個結點是紅色的,那麼它的兩個子節點都是黑色的(也就是說,不能有兩個相鄰的紅色結點)。
5. 對於每個結點,從該結點到其所有子孫葉結點的路徑中所包含的黑色結點數量必須相同。

數據項只能存儲在內部結點中(internal node)。我們所指的"葉結點"在其父結點中可能僅僅用一個NULL指針表示,但是將它也看作一個實際的結點有助於描述紅黑樹的插入與刪除算法,葉結點一律爲黑色。

定理:一棵擁有n個內部結點的紅黑樹的樹高h<=2log(n+1)

(譯者:我認爲原文中的有關上述定理的證明是錯誤的,下面的證明方法是參考CLRS中的證明寫出的。)

證明:首先定義一顆紅黑樹的黑高度Bh爲:從這顆紅黑樹的根結點(但不包括這個根結點)到葉結點的路徑上包含的黑色結點(注意,包括葉結點)數量。另外規定葉結點的黑高度爲0。
下面我們首先證明一顆有n個內部結點的紅黑樹滿足n>=2^Bh-1。這可以用數學歸納法證明,施歸納於樹高h。當h=0時,這相當於是一個葉結點,黑高度Bh爲0,而內部結點數量n爲0,此時0>=2^0-1成立。假設樹高h<=t時,n>=2^Bh-1成立,我們記一顆樹高爲t+1的紅黑樹的根結點的左子樹的內部結點數量爲nl,右子樹的內部結點數量爲nr,記這兩顆子樹的黑高度爲Bh'(注意這兩顆子樹的黑高度必然一樣),顯然這兩顆子樹的樹高<=t,於是有nl>=2^Bh'-1以及nr>=2^Bh'-1,將這兩個不等式相加有nl+nr>=2^(Bh'+1)-2,將該不等式左右加1,得到n>=2^(Bh'+1)-1,很顯然Bh'+1>=Bh,於是前面的不等式可以變爲n>=2^Bh-1,這樣就證明了一顆有n個內部結點的紅黑樹滿足n>=2^Bh-1。
下面我們完成剩餘部分的證明,記紅黑樹樹高爲h。我們先證明Bh>=h/2。在任何一條從根結點到葉結點的路徑上(不包括根結點,但包括葉結點),假設其結點數量爲m,注意其包含的黑色結點數量即爲Bh。當m爲偶數時,根據性質5可以看出每一對兒相鄰的結點至多有一個紅色結點,所以有Bh>=m/2;而當m爲奇數時,這條路徑上除去葉結點後有偶數個結點,於是這些結點中的黑色結點數B'滿足B'>=(m-1)/2,將該不等式前後加1得出Bh>=(m+1)/2,可以進一步得出Bh>m/2,綜合m爲偶數的情況可以得出Bh>=m/2,而m在最大的情況下等於樹高h,因此可以證明Bh>=h/2。將Bh>=h/2代入n>=2^Bh-1,最終得到h<=2log(n+1)。證明完畢。


本文餘下的內容將闡釋如何在不破壞紅黑樹性質的前提下進行結點的插入與刪除,以及爲什麼插入與刪除的處理次數與樹高是成比例的,或者說是O(log n)。

Okasaki插入方法

首先用與二叉搜索樹一樣的方法將一個結點插入到紅黑樹中,並且顏色爲紅色。(這個新結點的子結點將是葉結點,根據定義,這些葉結點是黑色的。)此時,我們將或者破壞了性質2(根結點爲黑色)或者破壞了性質4(不能有兩個相鄰的紅色結點)。

如果新插入的結點是根結點的話(這意味着在插入前該紅黑樹是空的),我們僅僅將這個結點的顏色改爲黑色,插入操作就完成了。

如果性質4遭到了破壞,這一定是由於新插入結點的父結點也是紅色造成的。由於紅黑樹的根結點必須是黑色的,因此新插入的結點一定會存在一個祖父結點,並且根據性質4這個祖父結點必然是黑色的。此時,由新插入結點的祖父結點爲根的子樹的結構一共有四種可能性(譯者,前面這句話我沒有看明白原文,我是用我的理解寫出來的,如果有誤請指正。),如下面的圖解所示。在Okasaki插入方法中,每一種可能出現的子樹都被轉換爲圖解正中間的那種子樹形式。



(A,B,C與D表示任意的子樹。我們曾經說過新插入結點的子結點一定是葉結點,但很快我們就會看到上面的圖解適用於更普遍的情況)。

首先,請注意在變換的過程中<AxByCzD>的順序保持不變。

另外,注意該變換不會改變從這顆子樹的父結點到這顆子樹中任何一個葉結點的路徑中黑色結點的數量(當然前提是這顆子樹有父結點)。我們再一次遇到了這樣的情形:即該紅黑樹只有可能違反性質2(如果y是根結點)或性質4(如果y的父結點是紅色的),但這次變換帶來了一個好處,即我們現在距離紅黑樹的根結點靠近了兩步。我們可以重複這種操作直到:或者y的父結點爲黑色,在這種情況下插入操作完成;或者y成爲根結點,在此情況下我們將y染爲黑色後插入操作完成。(將根結點染爲黑色會對每條從根結點到葉結點的路徑增加相同數量的黑色結點,因此如果在染色操作之前性質5沒有遭到破壞那麼操作之後也不會。)

上述步驟保持了紅黑樹的性質,並且所花的時間與樹高是成比例的,也即O(log n)。

旋轉

在紅黑樹中進行結構調整的操作常常可以用更清晰的術語"旋轉"操作來表達,圖解如下。



很顯然,在旋轉操作中<AxByC>的順序保持不變。因此,如果操作前該樹是一顆二叉搜索樹,而且結構調整時只使用了旋轉操作,那麼調整後該樹仍然是一顆二叉搜索樹。在本文的餘下部分,我們將僅僅使用旋轉操作對樹進行調整,因此我們無須再言明關於如何保持樹中元素的正確排序問題。

在下面的圖解中,Okasaki插入方法中的變換操作被表示爲一個或者兩個旋轉操作。



CLRS插入方法

CLRS中給出了一種比Okasaki插入方法更復雜但效率稍高的插入方法。它的時間複雜度仍然是O(log n),但在大O中的常數要更小一些。

CLRS插入方法與Okasaki插入方法一樣都是從標準的二叉搜索樹插入操作開始的,並且將這個新插入的結點染爲紅色,它們的區別在於如何處理遭到破壞的性質4(不能存在兩個相鄰的紅色結點)。我們要根據下端紅色結點的叔叔結點的顏色區分兩種情況。(下端紅色結點是指在一對兒紅色父結點/紅色子結點中的那個子結點。)讓我們先考慮叔叔結點爲黑色的情況。根據每個紅色結點是其父結點的左子結點還是右子結點,這種情況可以分爲四種子情況。下面的圖解展示瞭如何調整紅黑樹以及如何重新染色。



在這裏我們感興趣的是上面圖解中的方法與Okasaki方法的比較。它們有兩點不同。第一點是關於如何對最終的子樹(圖解中間的那個子樹)進行染色的。在Okasaki方法中,這顆子樹的根結點y被染成紅色而它的子結點被染成黑色,然而在CLRS方法中y被染成了黑色而它的子結點被染成了紅色。將y染成黑色意味着紅黑樹性質4(不能存在兩個相鄰的紅色結點)不會在y這一點遭到破壞,因此對樹的調整不需要向根結點的方向繼續進行下去。在此情況下,CLRS插入方法最多需要進行兩次旋轉操作即可完成插入。

第二點不同是在這種情況下CLRS方法必須滿足一個先決條件,即下端紅色結點的叔叔結點必須是黑色的。在上面的圖解中我們可以很清楚地看出,如果那個叔叔結點(即子樹A或者D的根結點)是紅色的,那麼最終的樹中將存在兩個相鄰的紅色結點,因此這種方法不能適用於叔叔結點爲紅色的情況。

下面我們考慮下端紅色結點的叔叔結點爲紅色的情況。在這種情況下我們將上端紅色結點和它的兄弟結點(即下端紅色結點的叔叔結點)染爲黑色並且將它們的父結點染爲紅色。樹的結構並沒有進行調整。這時根據下端紅色結點是其父結點的左子結點還是右子結點以及上端紅色結點是其父結點的左子結點還是右子結點可以分出四種情況,但是這四種情況從本質上來說都是相同的。下面圖解只描述了一種情況:



很容易看出,在這種操作的過程中從樹的根結點到葉結點的路徑中的黑色結點數量沒有發生變化。在此操作之後,紅黑數的性質只有可能在該子樹的根結點同時也是整個樹的根結點或者該子樹的父結點是紅色的情況下才會遭到破壞。換句話說,我們又將開始重複上述操作,但我們距離樹的根結點又靠近了兩步。照這樣不斷重複該步驟直到:或者(i)z的父結點爲黑色,此時插入操作結束;(ii)z成爲根結點,我們將它染爲黑色之後插入操作結束;或者(iii)我們遇到了下端紅色結點的叔叔結點爲黑色的情況,這時我們只要做一或兩次旋轉操作即可完成插入。在最壞的情況下,我們必須對新插入的結點到根結點的路徑上的每個結點進行染色操作,此時需要的操作數爲O(log n)。

刪除

爲了從紅黑樹中刪除一個結點,我們將從一顆標準二叉搜索樹的刪除操作開始(參見CLRS,第12章)。我們回顧一下標準二叉搜索樹的刪除操作的三種情況:

1. 要刪除的結點沒有子結點。在這種情況下,我們直接將它刪除就可以了。如果這個結點是根結點,那麼這顆樹將成爲空樹;否則,將它的父結點中相應的子結點指針賦值爲NULL。
2. 要刪除的結點有一個子結點。與上面一樣,直接將它刪除。如果它是根結點,那麼它的子結點變爲根結點;否則,將它的父結點中相應的子結點指針賦值爲被刪除結點的子結點的指針。
3. 要刪除的結點有兩個子結點。在這種情況下,我們先找到這個結點的後繼結點(successor),也就是它的右子樹中最小的那個結點。然後我們將這兩個結點中的數據元素互換,之後刪除這個後繼結點。由於這個後繼結點不可能有左子結點,因此刪除該後繼結點的操作必然會落入上面兩種情況之一。

注意,在樹中被刪除的結點並不一定是那個最初包含要刪除的數據項的那個結點。但出於重建紅黑樹性質的目的,我們只關心最終被刪除的那個結點。我們稱這個結點爲v,並稱它的父結點爲p(v)。

v的子結點中至少有一個爲葉結點。如果v有一個非葉子結點,那麼v在這顆樹中的位置將被這個子結點取代;否則,它的位置將被一個葉結點取代。我們用u來表示二叉搜索樹刪除操作後在樹中取代了v的位置的那個結點。如果u是葉結點,那麼我們可以確定它是黑色的。

如果v是紅色的,那麼刪除操作就完成了---因爲這種刪除不會破壞紅黑樹的任何性質。所以,我們下面假定v是黑色的。刪除了v之後,從根結點到v的所有子孫葉結點的路徑將會比樹中其它的從根結點到葉結點的路徑擁有更少的黑色結點,這會破壞紅黑樹的性質5。另外,如果p(v)與u都是紅色的,那麼性質4也會遭到破壞。但實際上我們解決性質5遭到破壞的方案在不用作任何額外工作的情況下就可以同時解決性質4遭到破壞的問題,所以從現在開始我們將集中精力考慮性質5的問題。

讓我們在頭腦中給u打上一個黑色記號(black token)。這個記號表示從根結點到這個帶記號結點的所有子孫葉結點的路徑上都缺少一個黑色結點(在一開始,這是由於v被刪除了)。我們會將這個記號一直朝樹的頂部移動直到性質5重新恢復。在下面的圖解中用一個黑色的方塊表示這個記號。如果帶有這個記號的結點是黑色的,那麼我們稱之爲雙黑色結點(doubly black node)。

注意這個記號只是一個概念上的東西,在樹的數據結構中並不存在物理實現。

我們要區分四種不同的情況。

A. 如果帶記號的結點是紅色的或者它是樹的根結點(或兩者皆是),只要將它染爲黑色就可以完成刪除操作。注意,這樣就會恢復紅黑樹的性質4(不能存在兩個相鄰的紅色結點)。而且,性質5也會被恢復,因爲這個記號表示從根結點到該結點的所有子孫葉結點的路徑需要增加一個黑色結點以便使這些路徑與其它的根結點到葉結點路徑所包含的黑色結點數量相同。通過將這個紅色結點改變爲黑色,我們就在這些缺少一個黑色結點的路徑上添加了一個黑色結點。

如果帶記號的結點是根結點並且爲黑色,那麼直接將這個標記丟掉就可以了。在這種情況下,樹中每條從根結點到葉結點的路徑的黑色結點數量都比刪除操作前少了一個,並且依舊保持住了性質5。

在餘下的情況裏,我們可以假設這個帶記號的結點是黑色的,並且不是根結點。

B. 如果這個雙黑色結點的兄弟結點以及兩個侄子結點都是黑色的,那麼我們就將它的兄弟結點染爲紅色之後將這個記號朝樹根的方向移動一步。

下面的圖解展示了兩種可能出現的子情況。環繞y的虛線表示在此並我們不關心y的顏色,而在A,B,C和D的上面的小圓圈表示這些子樹的根結點是黑色的(譯者:注意這個雙黑色結點必然會有兩個非葉結點的侄子結點。這是因爲這個雙黑色結點的記號表示從根結點到該結點的所有子孫葉結點的路徑中的黑色結點數量都比其它的根結點到葉結點路徑所包含的黑色結點數量少1,而該雙黑色結點本身就是一個黑色結點,因此從它的兄弟結點到其子孫葉結點的路徑上的黑色結點數量必然要大於1,我們很容易看出如果其兄弟結點的任何一個子結點爲葉結點的話這一點是不可能滿足的,因此這個雙黑色結點的必然會有兩個非葉結點的侄子結點)。



將那個兄弟結點染爲紅色,就會從所有到該結點的子孫葉結點的路徑上去掉一個黑色結點,因此現在這些路徑上的黑色結點數量與到雙黑色結點的子孫葉結點的路徑上的黑色結點數量一致了。我們將這個記號向上移動到y,這表明現在所有到y的子孫葉結點的路徑上缺少一個黑色結點。此時問題仍然沒有得到解決,但我們又向樹根推進了一步。

很顯然,只有帶記號的結點的兩個侄子結點都是黑色時才能進行上述操作,這是因爲如果有一個侄子結點是紅色的那麼該操作會導致出現兩個相鄰的紅色結點。

C. 如果帶記號的結點的兄弟結點是紅色的,那麼我們就進行一次旋轉操作並改變結點顏色。下面的圖解展示了兩種可能出現的情況:



注意上面的操作並不會改變從根結點到任何葉結點路徑上的黑色結點數量,並且它確保了在操作之後這個雙黑色結點的兄弟結點是黑色的,這使得後續的操作或者屬於情況B,或者屬於情況D。

由於這個記號比起操作前離樹的根結點更遠了,所以看起來似乎我們向後倒退了。但請注意現在這個雙黑色結點的父結點是紅色的了,所以如果下一步操作屬於情況B,那麼這個記號將會向上移動到那個紅色結點,然後我們只要將它染爲黑色就完成了。此外,下面將會展示,在情況D下,我們總是能夠將這個記號消耗掉從而完成刪除操作。因此這種表面上的倒退現象實際上意味着刪除操作就快要完成了。

D. 最終,我們遇到了雙黑色結點有一個黑色兄弟結點並至少一個侄子結點是紅色的情況。我們下面給出一個結點x的近侄子結點(near nephew)的定義:如果x是其父結點的左子結點,那麼x的兄弟結點的左子結點爲x的近侄子結點,否則x的兄弟結點的右子結點爲x的近侄子結點;而另一個侄子結點則爲x的遠侄子結點(far nephew)。(在下面的圖解中可以看出,x的近侄子結點要比它的遠侄子結點距離x更近。)

現在我們會遇到兩種子情況:(i)雙黑色結點的遠侄子結點是黑色的,在此情況下它的近侄子結點一定是紅色的;(ii)遠侄子結點是紅色的,在此情況下它的近侄子結點可以爲任何顏色。如下面的圖解所示,子情況(i)可以通過一次旋轉和變色轉換爲子情況(ii),而在子情況(ii)下只要通過一次旋轉和變色就可以完成刪除操作。根據雙黑色結點是其父結點的左子結點還是右子結點,下面圖解中的兩行顯示出兩種對稱的形式。



在這種情況下我們生成了一個額外的黑色結點,記號被丟掉,刪除操作完成。從上面圖解中很容易看出,所有到帶記號結點的子孫葉結點的路徑上的黑色結點數量增加了1,而其它的路徑上的黑色結點數量保持不變。很顯然,在此刻紅黑樹的任何性質都沒有遭到破壞。

將上面的所有情況綜合起來,我們可以看出在最壞的情況下我們必須沿着從葉結點到根結點的路徑每次都執行常量次數的操作,因此刪除操作的時間複雜度爲O(log n)。

 
附  AVL樹的比較
 
我還一直沉浸於2.4的AVL樹,殊不知,早已經是過年貨了(各位新春愉快!!),2.6已經使用Red-Black-Tree,感嘆
不是我不明白,世界變得太快,既然是AVL樹則比較一下:
簡介:
AVL樹又稱高度平衡的二叉搜索樹,是1962年由兩位俄羅斯的數學家G.M.Adel'son-Vel,sky和E.M.Landis提出
的.引入二叉樹的目的是爲了提高二叉樹的搜索的效率,減少樹的平均搜索長度.爲此,就必須每向二叉樹插入
一個結點時調整樹的結構,使得二叉樹搜索保持平衡,從而可能降低樹的高度,減少的平均樹的搜索長度.

AVL樹的定義:
一棵AVL樹滿足以下的條件:
1>它的左子樹和右子樹都是AVL樹
2>左子樹和右子樹的高度差不能超過1
從條件1可能看出是個遞歸定義,如GNU一樣.

性質:
1>一棵n個結點的AVL樹的其高度保持在0(log2(n)),不會超過3/2log2(n+1)
2>一棵n個結點的AVL樹的平均搜索長度保持在0(log2(n)).
3>一棵n個結點的AVL樹刪除一個結點做平衡化旋轉所需要的時間爲0(log2(n)).

從1這點來看紅黑樹是犧牲了嚴格的高度平衡的優越條件爲代價紅黑樹能夠以O(log2 n)的時間複雜度進行搜索、插入、刪除操作。此外,由於它的設計,任何不平衡都會在三次旋轉之內解決。當然,還有一些更好的,但實現起來更復雜的數據結構能夠做到一步旋轉之內達到平衡,但紅黑樹能夠給我們一個比較“便宜”的解決方案。紅黑樹的算法時間複雜度和AVL相同,但統計性能比AVL樹更高.
看看人家怎麼評價的:
AVL trees are actually easier to implement than RB trees because there are fewer cases. And AVL trees require O(1) rotations on an insertion, whereas red-black trees require O(lg n).
In practice, the speed of AVL trees versus red-black trees will depend on the data that you're inserting. If your data is well distributed, so that an unbalanced binary tree would generally be acceptable (i.e. roughly in random order), but you want to handle bad cases anyway, then red-black trees will be faster because they do less unnecessary rebalancing of already acceptable data.On the other hand, if a pathological insertion order (e.g. increasing order of key) is common, then AVL trees will be faster, because the stricter balancing rule will reduce the tree's height.
Splay trees might be even faster than either RB or AVL trees,depending on your data access distribution. And if you can use a hash instead of a tree, then that'll be fastest of all.

試着翻譯一下:
<pre>
由於AVL樹種類較少所以比紅黑樹實際上更容易實現.而且ALV樹在旋轉插入所需要的複雜度爲0(1),而紅
黑樹則需要的複雜度爲0(lgn).
實際上插入AVL樹和紅黑樹的速度取決於你所插入的數據.如果你的數據分佈較好,則比較宜於採用AVL樹(例如隨機產生系列數),但是如果你想處理比較雜亂的情況,則紅黑樹是比較快的,因爲紅黑樹對已經處理好的數據重新平衡減少了不心要的操作.另外一方面,如果是一種非尋常的插入系列比較常見(比如,插入密鑰系列),則AVL樹比較快,因爲它的嚴格的平衡規則將會減少樹的高度.
Splay樹可能比紅黑樹和AVL樹還要快這也取決於你所訪問的數據分佈,如果你用哈希表來代替一棵樹,則
將所以的樹還要快.
</pre>
Splay樹是什麼樹,我不是很清楚,我沒有詳細的查閱.

感受一下帶來的變革
//--><pre>
/*
* 翻一下老皇曆(2.4)
*/
struct vm_area_struct* find_vma(struct mm_struct* mm,unsigned long addr)
{
struct vm_area_struct* vma = NULL;
if(mm)
{
/*
* check the cache first.
*/
/*
* (Check hit rate is typically around 35%.)
*/
/*
* 首先查找一下最近一次訪問的虛地址空間是不否是 CACHE中
*/
vma = mm->mmap_cache;
if(!(vma && vma->vm_end > addr && vma->vm_start<addr))
{
/*
* miss hit 未命中,繼續查找線性表或者是AVL樹
*/
if(!mm->mmap_val)
{
/*
* go though the liner list
*/
vma = mm->mmap;
while(vma && vma->vm_end <= addr)
{
vma = vam->vma_next;
}
}
else
{
/*
* Then go though the AVL tree quickly
*/
struct vm_area_struct* tree = mm->mmap_avl;
vam = NULL;
for(;;)
{
if(tree == vm_avl_empty)
{
/*
* 結點爲空,失敗
*/
break;
}
if(tree->vm_end > addr)
{
vma = tree;
if(tree->vm_start <= addr)
{
/*
* 找到,快速退出循環
*/
break;
}
tree = tree->vm_avl_left;
}
else
{
tree = tree->vm_avl_right;
}
}
}
if(vma)
{
/*
* 查找成功,記在CACHE中
*/
mm->mmap_cache = vma;
}
}
}
return vma;

}

//<--</pre>

/*
* 再貼新年曆(2.6)
*/
//--><pre1>
struct vm_area_struct * find_vma(struct mm_struct * mm, unsigned long addr)
{
struct vm_area_struct *vma = NULL;

if (mm) {
/* Check the cache first. */
/* (Cache hit rate is typically around 35%.) */
/*
* 首先查找一下最近一次訪問的虛地址空間是不否是 CACHE中
*/
vma = mm->mmap_cache;
if (!(vma && vma->vm_end > addr && vma->vm_start <= addr)) {
struct rb_node * rb_node;

/*
* miss hit 未命中,直接查找red-black tree
*/
rb_node = mm->mm_rb.rb_node;
vma = NULL;

while (rb_node) {
struct vm_area_struct * vma_tmp;

vma_tmp = rb_entry(rb_node,
struct vm_area_struct, vm_rb);

if (vma_tmp->vm_end > addr) {
vma = vma_tmp;
if (vma_tmp->vm_start <= addr)
break;
rb_node = rb_node->rb_left;
} else
rb_node = rb_node->rb_right;
}
/*
* 查找成功,記在CACHE中
*/
if (vma)
mm->mmap_cache = vma;
}
}
return vma;
}

//<--</pre1>

在這兒只是做了一些小的方面的比較和在內核中的真正的應用,很多的地方沒有分析到,還
望各位同仁多多指正和拓展.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章