1. 程式人生 > >【演算法】紅黑樹的講解及插入刪除演算法實現原理

【演算法】紅黑樹的講解及插入刪除演算法實現原理

【轉】【經典】

導讀: 
  linux核心中的使用者態地址空間管理使用了紅黑樹(red-black tree)這種資料結構,我想一定有許多人在這種資料結構上感到困惑,我也曾經為此查閱了許多資料以便了解紅黑樹的原理。最近我在一個外國網站上看到一篇 講解紅黑樹的文章,覺得相當不錯,不敢獨享,於是翻譯成中文供所有核心版的弟兄們參考。由於本人水平有限,難免有出錯之處,歡迎大家指正。 
  原文網址:http://sage.mc.yu.edu/kbeen/teaching/algorithms/resources/red-black-tree.html 
   
  加兩個鏈結地址: 
  紅黑樹的實地使用 
    http://www.linuxforum.net/forum/showthreaded.php?Cat=&Board=program&Number=556347&page=0&view=collapsed&sb=5&o=31&fpart=&vc= 
  Splay樹的介紹 
   http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=609842&page=&view=&sb=&o=&vc=1 
  紅黑樹的定義

 
  正如在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& gt;=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表示任意的子樹。我們曾經說過新插入結點的子結點一定是葉結點,但很快我們就會看到上面的圖解適用於更普遍的情況)。 
  首先,請注意在變換的過程中   另外,注意該變換不會改變從這顆子樹的父結點到這顆子樹中任何一個葉結點的路徑中黑色結點的數量(當然前提是這顆子樹有父結點)。我們再一次遇到了這樣 的情形:即該紅黑樹只有可能違反性質2(如果y是根結點)或性質4(如果y的父結點是紅色的),但這次變換帶來了一個好處,即我們現在距離紅黑樹的根結點 靠近了兩步。我們可以重複這種操作直到:或者y的父結點為黑色,在這種情況下插入操作完成;或者y成為根結點,在此情況下我們將y染為黑色後插入操作完 成。(將根結點染為黑色會對每條從根結點到葉結點的路徑增加相同數量的黑色結點,因此如果在染色操作之前性質5沒有遭到破壞那麼操作之後也不會。) 
  上述步驟保持了紅黑樹的性質,並且所花的時間與樹高是成比例的,也即O(log n)。 
  旋轉 
  在紅黑樹中進行結構調整的操作常常可以用更清晰的術語"旋轉"操作來表達,圖解如下。 
   
   
  很顯然,在旋轉操作中的順序保持不變。因此,如果操作前該樹是一顆二叉搜尋樹,而且結構調整時只使用了旋轉操作,那麼調整後該樹仍然是一顆二叉搜尋樹。在本文的餘下部分,我們將僅僅使用旋轉操作對樹進行調整,因此我們無須再言明關於如何保持樹中元素的正確排序問題。 
  在下面的圖解中,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是葉結點,那麼我們可以確定它是黑色的。

【圖--額外補充---p(v)-》v-》u,注意:根據上面的推斷,v不一定有子節點,有的話一定是右子節點】

   如果v是紅色的,那麼刪除操作就完成了---因為這種刪除不會破壞紅黑樹的任何性質。


【沒有右子節點】


【有右子節點】

【v是紅色時候,刪除操作的兩種情況】

        所以,我們著重考慮當v是黑色的情況。我們下面假定v是黑色的。刪除了v之後,從根結點到v的所有 子孫葉結點的路徑將會比樹中其它的從根結點到葉結點的路徑擁有更少的黑色結點,這會破壞紅黑樹的性質5。另外,如果p(v)與u都是紅色的,那麼性質4也 會遭到破壞。但實際上我們解決性質5遭到破壞的方案在不用作任何額外工作的情況下就可以同時解決性質4遭到破壞的問題,所以從現在開始我們將集中精力考慮 性質5的問題。 
  讓我們在頭腦中給u打上一個黑色記號(black token)。這個記號表示從根結點到這個帶記號結點的所有子孫葉結點的路徑上都缺少一個黑色結點(在一開始,這是由於v被刪除了)。我們會將這個記號一 直朝樹的頂部移動直到性質5重新恢復。在下面的圖解中用一個黑色的方塊表示這個記號。如果帶有這個記號的結點是黑色的,那麼我們稱之為雙黑色結點 (doubly black node)。 
  注意這個記號只是一個概念上的東西,在樹的資料結構中並不存在物理實現。 
  我們要區分四種不同的情況。 
   A. 如果帶記號的結點是紅色的或者它是樹的根結點(或兩者皆是),只要將它染為黑色就可以完成刪除操作。注意,這樣就會恢復紅黑樹的性質4(不能存在兩個相鄰 的紅色結點)。而且,性質5也會被恢復,因為這個記號表示從根結點到該結點的所有子孫葉結點的路徑需要增加一個黑色結點以便使這些路徑與其它的根結點到葉 結點路徑所包含的黑色結點數量相同。通過將這個紅色結點改變為黑色,我們就在這些缺少一個黑色結點的路徑上添加了一個黑色結點。 
  如果帶記號的結點是根結點並且為黑色,那麼直接將這個標記丟掉就可以了。在這種情況下,樹中每條從根結點到葉結點的路徑的黑色結點數量都比刪除操作前少了一個,並且依舊保持住了性質5。 
  在餘下的情況裡,我們可以假設這個帶記號的結點是黑色的,並且不是根結點。 


【額外補圖--情況A示意圖】

【補充說明:很多人可能會有,萬一只是刪除一個單獨的黑色節點,譬如下圖所示的0.0節點,那麼如何辦呢?


    			/**
    			 * 當刪除的真實節點為黑色,只能將NIL節點拿出來參與到後續的修復調整工作中去。
    			 * */
解釋:請記住,原本紅黑樹的每一個葉節點都會有至少一個NIL(空)節點,這個節點是黑色的。我寫的程式裡面沒有開始沒有考慮nil節點,插入操作沒問題,但是刪除操作的時候遇到上述問題我採取的解決方案是,遇到這個問題就新增一個nil節點,將該nil節點作為黑色節點參與到下面幾種情況的調整,調整完以後就刪除這個nil節點,對性質完全沒有影響。   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)下只要通過一次旋轉和變色就可以 完成刪除操作。根據雙黑色結點是其父結點的左子結點還是右子結點,下面圖解中的兩行顯示出兩種對稱的形式。 
   

相關推薦

no