程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> C#與數據結構--樹論--紅黑樹(Red Black Tree)(上)

C#與數據結構--樹論--紅黑樹(Red Black Tree)(上)

編輯:關於C#

介紹

今天我們來介紹另一種平衡二叉樹:紅黑樹(Red Black Tree),紅黑樹由Rudolf Bayer於1972年發明,當時被稱為平衡二叉B樹(symmetric binary B-trees),1978年被Leonidas J. Guibas 和 Robert Sedgewick改成一個比較摩登的名字:紅黑樹。

紅黑樹和之前所講的AVL樹類似,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能。自從紅黑樹出來後,AVL樹就被放到了博物館裡,據說是紅黑樹有更好的效率,更高的統計性能。不過在我了解了紅黑樹的實現原理後,並不相信這是真的,關於這一點我們會在後面進行討論。

紅黑樹和AVL樹的區別在於它使用顏色來標識結點的高度,它所追求的是局部平衡而不是AVL樹中的非常嚴格的平衡。之前我們在講解AVL樹時,已經領教過AVL樹的復雜,但AVL樹的復雜比起紅黑樹來說簡直是小巫見大巫。紅黑樹是真正的變態級數據結構。

首先來一個Silverlight做的紅黑樹的動畫,它有助於幫你理解什麼是紅黑樹。這裡需要注意,必須安裝Silverlight 2.0 RTW 才能正常運行游戲,下載地址:

http://www.microsoft.com/silverlight/resources/install .aspx?v=2.0

使用注意事項:

l 結點只接收整數,如果在添加和刪除操作中輸入非法字串,則會隨機添加或刪除一個0~99之間的整數。

l 可以不在編輯框中輸入數字,直接單擊添加和刪除按鈕進行添加和刪除操作。

l 可以拖動拖動條控制動畫速度。

紅黑樹的平衡

紅黑樹首先是一棵二叉查找樹,它每個結點都被標上了顏色(紅色或黑色),紅黑樹滿足以下5個性質:

1、每個結點的顏色只能是紅色或黑色。

2、根結點是黑色的。

3、每個葉子結點都帶有兩個空的黑色結點(被稱為黑哨兵),如果一個結點n的只有一個左孩子,那麼n的右孩子是一個黑哨兵;如果結點n只有一個右孩子,那麼n的左孩子是一個黑哨兵。

4、如果一個結點是紅的,則它的兩個兒子都是黑的。也就是說在一條路徑上不能出現相鄰的兩個紅色結點。

5、對於每個結點來說,從該結點到其子孫葉結點的所有路徑上包含相同數目的黑結點。

紅黑樹的這5個性質中,第3點是比較難理解的,但它卻非常有必要。我們看圖1中的左邊這張圖,如果不使用黑哨兵,它完全滿足紅黑樹性質,結點50到兩個葉結點8和葉結點82路徑上的黑色結點數都為2個。但如果加入黑哨兵後(如圖1右圖中的小黑圓點),葉結點的個數變為8個黑哨兵,根結點50到這8個葉結點路徑上的黑高度就不一樣了,所以它並不是一棵紅黑樹。

要看真正的紅黑樹請在以上動畫中添加幾個結點,看看是否滿足以上性質。

紅黑樹的旋轉操作

紅黑樹的旋轉操作和AVL樹一樣,分為Ll 、RR、LR、RL四種旋轉類型,差別在於旋轉完成後改變的是結點的顏色,而不是平衡因子。旋轉動畫演示請參考AVL這篇文章中的Flash動畫:

http://www.cnblogs.com/abatei/archive/2008/11/17/1335031.html 

紅黑樹上結點的插入

在討論紅黑樹的插入操作之前必須要明白,任何一個即將插入的新結點的初始顏色都為紅色。這一點很容易理解,因為插入黑點會增加某條路徑上黑結點的數目,從而導致整棵樹黑高度的不平衡。但如果新結點父結點為紅色時(如圖2所示),將會違返紅黑樹性質:一條路徑上不能出現相鄰的兩個紅色結點。這時就需要通過一系列操作來使紅黑樹保持平衡。

為了清楚地表示插入操作以下在結點中使用“新”字表示一個新插入的結點;使用“父”字表示新插入點的父結點;使用“叔”字表示“父”結點的兄弟結點;使用“祖”字表示“父”結點的父結點。插入操作分為以下幾種情況:

1、黑父

如圖3所示,如果新點的父結點為黑色結點,那麼插入一個紅點將不會影響紅黑樹的平衡,此時插入操作完成。紅黑樹比AVL樹優秀的地方之一在於黑父的情況比較常見,從而使紅黑樹需要旋轉的幾率相對AVL樹來說會少一些。

2.紅父

如果新點的父結點為紅色,這時就需要進行一系列操作以保證整棵樹紅黑性質。如圖3所示,由於父結點為紅色,此時可以判定,祖父結點必定為黑色。這時需要根據叔父結點的顏色來決定做什麼樣的操作。青色結點表示顏色未知。由於有可能需要根結點到新點的路徑上進行多次旋轉操作,而每次進行不平衡判斷的起始點(我們可將其視為新點)都不一樣。所以我們在此使用一個藍色箭頭指向這個起始點,並稱之為判定點。

2.1 紅叔

當叔父結點為紅色時,如圖4所示,無需進行旋轉操作,只要將父和叔結點變為黑色,將祖父結點變為紅色即可。但由於祖父結點的父結點有可能為紅色,從而違反紅黑樹性質。此時必須將祖父結點作為新的判定點繼續向上進行平衡操作。

需要注意,無論“父”在“叔”的左邊還是右邊,無論“新”是“父”的左孩子還是右孩子,它們的操作都完全一樣。

2.2 黑叔

當叔父結點為黑色時,需要進行旋轉,以下圖示了所有的旋轉可能

情形1:

情形2:

情形3:

情形4:

可以觀察到,當旋轉完成後,新的旋轉根全部為黑色,此時不需要再向上回溯進行平衡操作,插入操作完成。需要注意,上面四張圖的“叔”、“1”、“2”、“3”結點有可能為黑哨兵結點。

其實紅黑樹的插入操作不是很難,甚至比AVL樹的插入操作還更簡單些。但刪除操作就遠遠比AVL樹復雜得多,下面就介紹紅黑樹的刪除操作。

紅黑樹上結點的刪除

紅黑樹本身是一棵二叉查找樹,它的刪除和二叉查找樹的刪除類似。首先要找到真正的刪除點,當被刪除結點n存在左右孩子時,真正的刪除點應該是n的中序遍在前驅,關於這一點請復習二叉查找樹的刪除。如圖9所示,當刪除結點20時,實際被刪除的結點應該為18,結點20的數據變為18。

所以可以推斷出,在進行刪除操作時,真正的刪除點必定是只有一個孩子或沒有孩子的結點。而根據紅黑樹的性質可以得出以下兩個結論:

1、刪除操作中真正被刪除的必定是只有一個紅色孩子或沒有孩子的結點。

2、如果真正的刪除點是一個紅色結點,那麼它必定是一個葉子結點。

理解這兩點非常重要,如圖10所示,除了情況(a)外,其他任一種況結點N都無法滿足紅黑樹性質。

在以下討論中,我們使用藍色箭頭表示真正的刪除點,它也是旋轉操作過程中的第一個判定點;真正的刪除點使用“舊”標注,舊點所在位置將被它的的孩子結點所取代(最多只有一個孩子),我們使用“新”表示舊點的孩子結點。刪除操作可分為以下幾種情形:

1、舊點為紅色結點

若舊點為紅色結點,則它必定是葉子結點,直接刪除即可。如圖11所示

2、一紅一黑

當舊點為黑色結點,新點為紅色結點時,將新點取代舊點位置後,將新點染成紅色即可(如圖12所示)。這裡需要注意:舊點為紅色,新點為黑色的情況不可能存在。

3、雙黑

當舊點和新點都為黑色時(新點為空結點時,亦屬於這種情況),情況比較復雜,需要根據舊點兄弟結點的顏色來決定進行什麼樣的操作。我們使用“兄”來表示舊點的兄弟結點。這裡可分為紅兄和黑兄兩種情況:

3.1 紅兄

由於兄弟結點為紅色,所以父結點必定為黑色,而舊點被刪除後,新點取代了它的位置。下圖演示了兩種可能的情況:

紅兄的情況需要進行RR或LL型旋轉,然後將父結點染成紅色,兄結點染成黑色。然後重新以新點為判定點進行平衡操作。我們可以觀察到,旋轉操作完成後,判定點沒有向上回溯,而是降低了一層,此時變成了黑兄的情況。

3.2 黑兄

黑兄的情況最為復雜,需要根據黑兄孩子結點(這裡用“侄”表示)和父親結點的顏色來決定做什麼樣的操作。

3.2.1 黑兄二黑侄紅父

如圖14所示,這種情況比較簡單,只需將父結點變為黑色,兄結點變為黑色,新結點變為黑色即可,刪除操作到此結束。

3.2.2黑兄二黑侄黑父

如圖15所示,此時將父結點染成新結點的顏色,新結點染成黑色,兄結點染成黑色即可。當新結點為紅色時,父結點被染成紅色,此時需要以父結點為判定點繼續向上進行平衡操作。

3.2.3 黑兄紅侄

黑兄紅侄有以下四種情形,下面分別進行圖示:

情形1:

情形2:

情形3:

情形4:

由以上圖例所示,看完以上四張圖的兄弟有可能會有一個疑問,如果情形1和情形2中的兩個侄子結點都為紅色時,是該進行LL旋轉還是進行LR旋轉呢?答案是進行LL旋轉。情形3和情形4則是優先進行RR旋轉的判定。

紅黑樹的代碼實現

本以為紅黑樹的代碼非常容易,因為System.Collections.Generic.SortedDictionary類就是使用紅黑樹實現的,把代碼的算法部分摳出來就搞定了。但看了SortedDictionary源碼後有些失望,C#中真正實現紅黑樹的是TreeSet類,SortedDictionary只是在TreeSet的基礎上進一步抽象,加上了Key/Value泛型對。TreeSet使用了一種新的紅黑樹算法,它在搜索插入點和刪除點時預先進行旋轉和染色操作,從而避免插入和刪除後的回溯。這種算法看上去很美,但仔細想想,如果插入的是一個已經存在的結點,刪除的結點並不存在,那這些預平衡處理不是白做了嗎?更可怕的是如果在一條路徑上間隔進行一次插入和一次刪除,而這些操作沒有命中目標,那麼大家就會看到結點的顏色變來變去,這些都是無用功。來看看在尋找插入和刪除點的路徑上TreeSet每前進一步都要做些什麼:給四個變量賦值;判斷每個結點的兩個孩子結點的顏色。這種算法在《java數據結構和算法》這本書中有詳細講述,不過只講解了插入算法。另外國內也專門有一篇論文描述這個算法,他的測試結果是這種算法優於其他算法,估計測試時沒有不命中目標的情況發生。總之我並不相信這是一個好的算法。

為了證實我的想法,我不得不自己實現紅黑樹,實現思路跟AVL樹很類似,也是使用一個數組保存訪問路徑以進行回溯,當然,考慮到紅黑樹不嚴格的平衡,數組的長度設為64,這並不會給性能帶來什麼影響。過程很艱辛,需要做大量測試。很不幸,寫完後繼續做紅黑樹的Silverlight動畫時不小心把原來的代碼給覆蓋掉了,結點刪除部分的代碼丟失。當時幾乎崩潰,不過重寫並沒有我想象的那麼困難,很快完成,感覺思路清晰了很多,實現比原來也有了改進,感謝上帝!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved