30張圖帶你理解紅黑樹
30張圖帶你徹底理解紅黑樹
當在10億資料進行不到30次比較就能查詢到目標時,不禁感嘆程式設計之魅力!人類之偉大呀! —— 學紅黑樹有感。
終於,在學習了幾天的紅黑樹相關的知識後,我想把我所學所想和所感分享給大家。紅黑樹是一種比較難的資料結構,要完全搞懂非常耗時耗力,紅黑樹怎麼自平衡?什麼時候需要左旋或右旋?插入和刪除破壞了樹的平衡後怎麼處理?等等一連串的問題在學習前困擾著我。如果你在學習過程中也會存在我的疑問,那麼本文對你會有幫助,本文幫助你全面、徹底地理解紅黑樹!
本文將通過圖文的方式講解紅黑樹的知識點,並且不會涉及到任何程式碼,相信我,在懂得紅黑樹實現原理前,看程式碼會一頭霧水的,當原理懂了,程式碼也就按部就班寫而已,沒任何難度。
閱讀本文你需具備知識點:
- 二叉查詢樹
- 完美平衡二叉樹
事不宜遲,讓我們進入正題吧。
正文
紅黑樹也是二叉查詢樹,我們知道,二叉查詢樹這一資料結構並不難,而紅黑樹之所以難是難在它是自平衡的二叉查詢樹,在進行插入和刪除等可能會破壞樹的平衡的操作時,需要重新自處理達到平衡狀態。現在在腦海想下怎麼實現?是不是太多情景需要考慮了?嘖嘖,先別急,通過本文的學習後,你會覺得,其實也不過如此而已。好吧,我們先來看下紅黑樹的定義和一些基本性質。
紅黑樹定義和性質
紅黑樹是一種含有紅黑結點並能自平衡的二叉查詢樹。它必須滿足下面性質:
- 性質1:每個節點要麼是黑色,要麼是紅色。
- 性質2:根節點是黑色。
- 性質3:每個葉子節點(NIL)是黑色。
- 性質4:每個紅色結點的兩個子結點一定都是黑色。
- 性質5:任意一結點到每個葉子結點的路徑都包含數量相同的黑結點。
從性質5又可以推出:
- 性質5.1:如果一個結點存在黑子結點,那麼該結點肯定有兩個子結點
圖1就是一顆簡單的紅黑樹。其中Nil為葉子結點,並且它是黑色的。(值得提醒注意的是,在Java中,葉子結點是為null的結點。)
圖1 一顆簡單的紅黑樹
紅黑樹並不是一個完美平衡二叉查詢樹,從圖1可以看到,根結點P的左子樹顯然比右子樹高,但左子樹和右子樹的黑結點的層數是相等的,也即任意一個結點到到每個葉子結點的路徑都包含數量相同的黑結點(性質5)。所以我們叫紅黑樹這種平衡為黑色完美平衡。
介紹到此,為了後面講解不至於混淆,我們還需要來約定下紅黑樹一些結點的叫法,如圖2所示。
圖2 結點叫法約定
我們把正在處理(遍歷)的結點叫做當前結點,如圖2中的D,它的父親叫做父結點,它的父親的另外一個子結點叫做兄弟結點,父親的父親叫做祖父結點。
前面講到紅黑樹能自平衡,它靠的是什麼?三種操作:左旋、右旋和變色。
- 左旋:以某個結點作為支點(旋轉結點),其右子結點變為旋轉結點的父結點,右子結點的左子結點變為旋轉結點的右子結點,左子結點保持不變。如圖3。
- 右旋:以某個結點作為支點(旋轉結點),其左子結點變為旋轉結點的父結點,左子結點的右子結點變為旋轉結點的左子結點,右子結點保持不變。如圖4。
- 變色:結點的顏色由紅變黑或由黑變紅。
圖3 左旋
圖4 右旋
上面所說的旋轉結點也即旋轉的支點,圖4和圖5中的P結點。
我們先忽略顏色,可以看到旋轉操作不會影響旋轉結點的父結點,父結點以上的結構還是保持不變的。
左旋隻影響旋轉結點和其右子樹的結構,把右子樹的結點往左子樹挪了。
右旋隻影響旋轉結點和其左子樹的結構,把左子樹的結點往右子樹挪了。
所以旋轉操作是區域性的。另外可以看出旋轉能保持紅黑樹平衡的一些端詳了:當一邊子樹的結點少了,那麼向另外一邊子樹“借”一些結點;當一邊子樹的結點多了,那麼向另外一邊子樹“租”一些結點。
但要保持紅黑樹的性質,結點不能亂挪,還得靠變色了。怎麼變?具體情景又不同變法,後面會具體講到,現在只需要記住紅黑樹總是通過旋轉和變色達到自平衡。
balabala了這麼多,相信你對紅黑樹有一定印象了,那麼現在來考考你:
*思考題1:黑結點可以同時包含一個紅子結點和一個黑子結點嗎?* (答案見文末)
接下來先講解紅黑樹的查詢熱熱身。
紅黑樹查詢
因為紅黑樹是一顆二叉平衡樹,並且查詢不會破壞樹的平衡,所以查詢跟二叉平衡樹的查詢無異:
- 從根結點開始查詢,把根結點設定為當前結點;
- 若當前結點為空,返回null;
- 若當前結點不為空,用當前結點的key跟查詢key作比較;
- 若當前結點key等於查詢key,那麼該key就是查詢目標,返回當前結點;
- 若當前結點key大於查詢key,把當前結點的左子結點設定為當前結點,重複步驟2;
- 若當前結點key小於查詢key,把當前結點的右子結點設定為當前結點,重複步驟2;
如圖5所示。
圖5 二叉樹查詢流程圖
非常簡單,但簡單不代表它效率不好。正由於紅黑樹總保持黑色完美平衡,所以它的查詢最壞時間複雜度為O(2lgN),也即整顆樹剛好紅黑相隔的時候。能有這麼好的查詢效率得益於紅黑樹自平衡的特性,而這背後的付出,紅黑樹的插入操作功不可沒~
紅黑樹插入
插入操作包括兩部分工作:一查詢插入的位置;二插入後自平衡。查詢插入的父結點很簡單,跟查詢操作區別不大:
- 從根結點開始查詢;
- 若根結點為空,那麼插入結點作為根結點,結束。
- 若根結點不為空,那麼把根結點作為當前結點;
- 若當前結點為null,返回當前結點的父結點,結束。
- 若當前結點key等於查詢key,那麼該key所在結點就是插入結點,更新結點的值,結束。
- 若當前結點key大於查詢key,把當前結點的左子結點設定為當前結點,重複步驟4;
- 若當前結點key小於查詢key,把當前結點的右子結點設定為當前結點,重複步驟4;
如圖6所示。
圖6 紅黑樹插入位置查詢
ok,插入位置已經找到,把插入結點放到正確的位置就可以啦,但插入結點是應該是什麼顏色呢?答案是紅色。理由很簡單,紅色在父結點(如果存在)為黑色結點時,紅黑樹的黑色平衡沒被破壞,不需要做自平衡操作。但如果插入結點是黑色,那麼插入位置所在的子樹黑色結點總是多1,必須做自平衡。
所有插入情景如圖7所示。
圖7 紅黑樹插入情景
嗯,插入情景很多呢,8種插入情景!但情景1、2和3的處理很簡單,而情景4.2和情景4.3只是方向反轉而已,懂得了一種情景就能推出另外一種情景,所以總體來看,並不複雜,後續我們將一個一個情景來看,把它徹底搞懂。
另外,根據二叉樹的性質,除了情景2,所有插入操作都是在葉子結點進行的。這點應該不難理解,因為查詢插入位置時,我們就是在找子結點為空的父結點的。
在開始每個情景的講解前,我們還是先來約定下,如圖8所示。
圖8 插入操作結點的叫法約定
圖8的字母並不代表結點Key的大小。I表示插入結點,P表示插入結點的父結點,S表示插入結點的叔叔結點,PP表示插入結點的祖父結點。
好了,下面讓我們一個一個來分析每個插入的情景以其處理。
插入情景1:紅黑樹為空樹
最簡單的一種情景,直接把插入結點作為根結點就行,但注意,根據紅黑樹性質2:根節點是黑色。還需要把插入結點設為黑色。
處理:把插入結點作為根結點,並把結點設定為黑色。
插入情景2:插入結點的Key已存在
插入結點的Key已存在,既然紅黑樹總保持平衡,在插入前紅黑樹已經是平衡的,那麼把插入結點設定為將要替代結點的顏色,再把結點的值更新就完成插入。
處理:
- 把I設為當前結點的顏色
- 更新當前結點的值為插入結點的值
插入情景3:插入結點的父結點為黑結點
由於插入的結點是紅色的,當插入結點的黑色時,並不會影響紅黑樹的平衡,直接插入即可,無需做自平衡。
處理:直接插入。
插入情景4:插入結點的父結點為紅結點
再次回想下紅黑樹的性質2:根結點是黑色。如果插入的父結點為紅結點,那麼該父結點不可能為根結點,所以插入結點總是存在祖父結點。這點很重要,因為後續的旋轉操作肯定需要祖父結點的參與。
情景4又分為很多子情景,下面將進入重點部分,各位看官請留神了。
插入情景4.1:叔叔結點存在並且為紅結點
從紅黑樹性質4可以,祖父結點肯定為黑結點,因為不可以同時存在兩個相連的紅結點。那麼此時該插入子樹的紅黑層數的情況是:黑紅紅。顯然最簡單的處理方式是把其改為:紅黑紅。如圖9和圖10所示。
處理:
- 將P和S設定為黑色
- 將PP設定為紅色
- 把PP設定為當前插入結點
圖9 插入情景4.1_1
圖10 插入情景4.1_2
可以看到,我們把PP結點設為紅色了,如果PP的父結點是黑色,那麼無需再做任何處理;但如果PP的父結點是紅色,根據性質4,此時紅黑樹已不平衡了,所以還需要把PP當作新的插入結點,繼續做插入操作自平衡處理,直到平衡為止。
試想下PP剛好為根結點時,那麼根據性質2,我們必須把PP重新設為黑色,那麼樹的紅黑結構變為:黑黑紅。換句話說,從根結點到葉子結點的路徑中,黑色結點增加了。這也是唯一一種會增加紅黑樹黑色結點層數的插入情景。
我們還可以總結出另外一個經驗:紅黑樹的生長是自底向上的。這點不同於普通的二叉查詢樹,普通的二叉查詢樹的生長是自頂向下的。
插入情景4.2:叔叔結點不存在或為黑結點,並且插入結點的父親結點是祖父結點的左子結點
單純從插入前來看,也即不算情景4.1自底向上處理時的情況,叔叔結點非紅即為葉子結點(Nil)。因為如果叔叔結點為黑結點,而父結點為紅結點,那麼叔叔結點所在的子樹的黑色結點就比父結點所在子樹的多了,這不滿足紅黑樹的性質5。後續情景同樣如此,不再多做說明了。
前文說了,需要旋轉操作時,肯定一邊子樹的結點多了或少了,需要租或借給另一邊。插入顯然是多的情況,那麼把多的結點租給另一邊子樹就可以了。
插入情景4.2.1:插入結點是其父結點的左子結點
處理:
- 將P設為黑色
- 將PP設為紅色
- 對PP進行右旋
圖11 插入情景4.2.1
由圖11可得,左邊兩個紅結點,右邊不存在,那麼一邊一個剛剛好,並且因為為紅色,肯定不會破壞樹的平衡。
咦,可以把P設為紅色,I和PP設為黑色嗎?答案是可以!看過《演算法:第4版》的同學可能知道,書中講解的就是把P設為紅色,I和PP設為黑色。但把P設為紅色,顯然又會出現情景4.1的情況,需要自底向上處理,做多了無謂的操作,既然能自己消化就不要麻煩祖輩們啦~
插入情景4.2.2:插入結點是其父結點的右子結點
這種情景顯然可以轉換為情景4.2.1,如圖12所示,不做過多說明了。
處理:
- 對P進行左旋
- 把P設定為插入結點,得到情景4.2.1
- 進行情景4.2.1的處理
圖12 插入情景4.2.2
插入情景4.3:叔叔結點不存在或為黑結點,並且插入結點的父親結點是祖父結點的右子結點
該情景對應情景4.2,只是方向反轉,不做過多說明了,直接看圖。
插入情景4.3.1:插入結點是其父結點的右子結點
處理:
- 將P設為黑色
- 將PP設為紅色
- 對PP進行左旋
圖13 插入情景4.3.1
插入情景4.3.2:插入結點是其父結點的左子結點
處理:
- 對P進行右旋
- 把P設定為插入結點,得到情景4.3.1
- 進行情景4.3.1的處理
圖14 插入情景4.3.2
好了,講完插入的所有情景了。可能又同學會想:上面的情景舉例的都是第一次插入而不包含自底向上處理的情況,那麼上面所說的情景都適合自底向上的情況嗎?答案是肯定的。理由很簡單,但每棵子樹都能自平衡,那麼整棵樹最終總是平衡的。好吧,在出個習題,請大家拿出筆和紙畫下試試(請務必動手畫下,加深印象):
*習題1:請畫出圖15的插入自平衡處理過程。*(答案見文末)
圖15 習題1
紅黑樹刪除
紅黑樹插入已經夠複雜了,但刪除更復雜,也是紅黑樹最複雜的操作了。但穩住,勝利的曙光就在前面了!
紅黑樹的刪除操作也包括兩部分工作:一查詢目標結點;而刪除後自平衡。查詢目標結點顯然可以複用查詢操作,當不存在目標結點時,忽略本次操作;當存在目標結點時,刪除後就得做自平衡處理了。刪除了結點後我們還需要找結點來替代刪除結點的位置,不然子樹跟父輩結點斷開了,除非刪除結點剛好沒子結點,那麼就不需要替代。
二叉樹刪除結點找替代結點有3種情情景:
- 情景1:若刪除結點無子結點,直接刪除
- 情景2:若刪除結點只有一個子結點,用子結點替換刪除結點
- 情景3:若刪除結點有兩個子結點,用後繼結點(大於刪除結點的最小結點)替換刪除結點
補充說明下,情景3的後繼結點是大於刪除結點的最小結點,也是刪除結點的右子樹種最左結點。那麼可以拿前繼結點(刪除結點的左子樹最右結點)替代嗎?可以的。但習慣上大多都是拿後繼結點來替代,後文的講解也是用後繼結點來替代。另外告訴大家一種找前繼和後繼結點的直觀的方法(不知為何沒人提過,大家都知道?):把二叉樹所有結點投射在X軸上,所有結點都是從左到右排好序的,所有目標結點的前後結點就是對應前繼和後繼結點。如圖16所示。
圖16 二叉樹投射x軸後有序
接下來,講一個重要的思路:刪除結點被替代後,在不考慮結點的鍵值的情況下,對於樹來說,可以認為刪除的是替代結點!話很蒼白,我們看圖17。在不看鍵值對的情況下,圖17的紅黑樹最終結果是刪除了Q所在位置的結點!這種思路非常重要,大大簡化了後文講解紅黑樹刪除的情景!
圖17 刪除結點換位思路
基於此,上面所說的3種二叉樹的刪除情景可以相互轉換並且最終都是轉換為情景1!
- 情景2:刪除結點用其唯一的子結點替換,子結點替換為刪除結點後,可以認為刪除的是子結點,若子結點又有兩個子結點,那麼相當於轉換為情景3,一直自頂向下轉換,總是能轉換為情景1。(對於紅黑樹來說,根據性質5.1,只存在一個子結點的結點肯定在樹末了)
- 情景3:刪除結點用後繼結點(肯定不存在左結點),如果後繼結點有右子結點,那麼相當於轉換為情景2,否則轉為為情景1。
二叉樹刪除結點情景關係圖如圖18所示。
圖18 二叉樹刪除情景轉換
綜上所述,刪除操作刪除的結點可以看作刪除替代結點,而替代結點最後總是在樹末。有了這結論,我們討論的刪除紅黑樹的情景就少了很多,因為我們只考慮刪除樹末結點的情景了。
同樣的,我們也是先來總體看下刪除操作的所有情景,如圖19所示。
圖19 紅黑樹刪除情景
哈哈,是的,即使簡化了還是有9種情景!但跟插入操作一樣,存在左右對稱的情景,只是方向變了,沒有本質區別。同樣的,我們還是來約定下,如圖20所示。
圖20 刪除操作結點的叫法約定
圖20的字母並不代表結點Key的大小。R表示替代結點,P表示替代結點的父結點,S表示替代結點的兄弟結點,SL表示兄弟結點的左子結點,SR表示兄弟結點的右子結點。灰色結點表示它可以是紅色也可以是黑色。
值得特別提醒的是,R是即將被替換到刪除結點的位置的替代結點,在刪除前,它還在原來所在位置參與樹的子平衡,平衡後再替換到刪除結點的位置,才算刪除完成。
萬事具備,我們進入最後的也是最難的講解。
刪除情景1:替換結點是紅色結點
我們把替換結點換到了刪除結點的位置時,由於替換結點時紅色,刪除也了不會影響紅黑樹的平衡,只要把替換結點的顏色設為刪除的結點的顏色即可重新平衡。
處理:顏色變為刪除結點的顏色
刪除情景2:替換結點是黑結點
當替換結點是黑色時,我們就不得不進行自平衡處理了。我們必須還得考慮替換結點是其父結點的左子結點還是右子結點,來做不同的旋轉操作,使樹重新平衡。
刪除情景2.1:替換結點是其父結點的左子結點
刪除情景2.1.1:替換結點的兄弟結點是紅結點
若兄弟結點是紅結點,那麼根據性質4,兄弟結點的父結點和子結點肯定為黑色,不會有其他子情景,我們按圖21處理,得到刪除情景2.1.2.3(後續講解,這裡先記住,此時R仍然是替代結點,它的新的兄弟結點SL和兄弟結點的子結點都是黑色)。
處理:
- 將S設為黑色
- 將P設為紅色
- 對P進行左旋,得到情景2.1.2.3
- 進行情景2.1.2.3的處理
圖21 刪除情景2.1.1
刪除情景2.1.2:替換結點的兄弟結點是黑結點
當兄弟結點為黑時,其父結點和子結點的具體顏色也無法確定(如果也不考慮自底向上的情況,子結點非紅即為葉子結點Nil,Nil結點為黑結點),此時又得考慮多種子情景。
刪除情景2.1.2.1:替換結點的兄弟結點的右子結點是紅結點,左子結點任意顏色
即將刪除的左子樹的一個黑色結點,顯然左子樹的黑色結點少1了,然而右子樹又又紅色結點,那麼我們直接向右子樹“借”個紅結點來補充黑結點就好啦,此時肯定需要用旋轉處理了。如圖22所示。
處理:
- 將S的顏色設為P的顏色
- 將P設為黑色
- 將SR設為黑色
- 對P進行左旋
圖22 刪除情景2.1.2.1
平衡後的圖怎麼不滿足紅黑樹的性質?前文提醒過,R是即將替換的,它還參與樹的自平衡,平衡後再替換到刪除結點的位置,所以R最終可以看作是刪除的。另外圖2.1.2.1是考慮到第一次替換和自底向上處理的情況,如果只考慮第一次替換的情況,根據紅黑樹性質,SL肯定是紅色或為Nil,所以最終結果樹是平衡的。如果是自底向上處理的情況,同樣,每棵子樹都保持平衡狀態,最終整棵樹肯定是平衡的。後續的情景同理,不做過多說明了。
刪除情景2.1.2.2:替換結點的兄弟結點的右子結點為黑結點,左子結點為紅結點
兄弟結點所在的子樹有紅結點,我們總是可以向兄弟子樹借個紅結點過來,顯然該情景可以轉換為情景2.1.2.1。圖如23所示。
處理:
- 將S設為紅色
- 將SL設為黑色
- 對S進行右旋,得到情景2.1.2.1
- 進行情景2.1.2.1的處理
圖23 刪除情景2.1.2.2
刪除情景2.1.2.3:替換結點的兄弟結點的子結點都為黑結點
好了,此次兄弟子樹都沒紅結點“借”了,兄弟幫忙不了,找父母唄,這種情景我們把兄弟結點設為紅色,再把父結點當作替代結點,自底向上處理,去找父結點的兄弟結點去“借”。但為什麼需要把兄弟結點設為紅色呢?顯然是為了在P所在的子樹中保證平衡(R即將刪除,少了一個黑色結點,子樹也需要少一個),後續的平衡工作交給父輩們考慮了,還是那句,當每棵子樹都保持平衡時,最終整棵總是平衡的。
處理:
- 將S設為紅色
- 把P作為新的替換結點
- 重新進行刪除結點情景處理
圖24 情景2.1.2.3
刪除情景2.2:替換結點是其父結點的右子結點
好啦,右邊的操作也是方向相反,不做過多說明了,相信理解了刪除情景2.1後,肯定可以理解2.2。
刪除情景2.2.1:替換結點的兄弟結點是紅結點
處理:
- 將S設為黑色
- 將P設為紅色
- 對P進行右旋,得到情景2.2.2.3
- 進行情景2.2.2.3的處理
圖25 刪除情景2.2.1
刪除情景2.2.2:替換結點的兄弟結點是黑結點
刪除情景2.2.2.1:替換結點的兄弟結點的左子結點是紅結點,右子結點任意顏色
處理:
- 將S的顏色設為P的顏色
- 將P設為黑色
- 將SL設為黑色
- 對P進行右旋
圖26 刪除情景2.2.2.1
刪除情景2.2.2.2:替換結點的兄弟結點的左子結點為黑結點,右子結點為紅結點
處理:
- 將S設為紅色
- 將SR設為黑色
- 對S進行左旋,得到情景2.2.2.1
- 進行情景2.2.2.1的處理
圖27 刪除情景2.2.2.2
刪除情景2.2.2.3:替換結點的兄弟結點的子結點都為黑結點
處理:
- 將S設為紅色
- 把P作為新的替換結點
- 重新進行刪除結點情景處理
圖28 刪除情景2.2.2.3
綜上,紅黑樹刪除後自平衡的處理可以總結為:
- 自己能搞定的自消化(情景1)
- 自己不能搞定的叫兄弟幫忙(除了情景1、情景2.1.2.3和情景2.2.2.3)
- 兄弟都幫忙不了的,通過父母,找遠方親戚(情景2.1.2.3和情景2.2.2.3)
哈哈,是不是跟現實中很像,當我們有困難時,首先先自己解決,自己無力了總兄弟姐妹幫忙,如果連兄弟姐妹都幫不上,再去找遠方的親戚了。這裡記憶應該會好記點~
最後再做個習題加深理解(請不熟悉的同學務必動手畫下):
***習題2:請畫出圖29的刪除自平衡處理過程。
習題2
寫在後面
耗時良久,終於寫完了~自己加深了紅黑樹的理解的同時,也希望能幫助大家。如果你之前沒學習過紅黑樹,看完這篇文章後可能還存在很多疑問,如果有疑問可以在評論區寫出來,我會盡自己所能解答。另外給大家推薦一個支援紅黑樹線上生成的網站,來做各種情景梳理很有幫助:線上生成紅黑樹。(刪除操作那個把替代結點看作刪除結點思路就是我自己在用這個網站時自己頓悟的,我覺得這樣講解更容易理解。)
少了程式碼是不是覺得有點空虛?哈哈,後續我會寫關於Java和HashMap和TreeMap的文章,裡面都有紅黑樹相關的知識。相信看了這篇文章後,再去看Java和HashMap和TreeMap的原始碼絕對沒難度!
最後來看下思考題和習題的答案吧。
思考題和習題答案
*思考題1:黑結點可以同時包含一個紅子結點和一個黑子結點嗎?*
答:可以。如下圖的F結點:
*習題1:請畫出圖15的插入自平衡處理過程。*
答:
*習題2:請畫出圖29的刪除自平衡處理過程。*
答:
紅黑樹
學習:第2遍
紅黑樹的java實現(備註:中序遍歷、查詢、最大、最小、前驅、後繼與二叉樹基本一致)
package com.m.suan_pai;
public class RBTree {
/**
* 根節點
*/
private static Node root;
/**
* nil節點是紅黑樹的葉子節點不同於二叉樹的葉子節點
* <p>
* 顏色為黑色,key、left、right、parent可以是任意允許的值
* </p>
* 這裡key設定為0,left、right、parent為null
*/
private Node nil = new Node(true);
static class Node {
int key;
Node left, right, parent;
boolean color;// true黑,false紅
public Node(int key) {
this.key = key;
}
public Node(boolean color) {
this.color = color;
}
public boolean equals(Node node) {
return this.key == node.key;
}
}
public RBTree(int key) {
root = new Node(key);
}
/**
* 中序遍歷
*
* @param node 根節點
*/
public void inOrderTreeWalk(Node node) {
if (node != null && !node.equals(nil)) {
inOrderTreeWalk(node.left);
System.out.print((node.color == true ? "黑" : "紅") + node.key + ",");
inOrderTreeWalk(node.right);
}
}
/**
* 查詢
*
* @param node 根節點
* @param key 查詢值
* @return
*/
public Node treeSearch(Node node, int key) {
while (node != null && key != node.key) {
if (key < node.key) {
node = node.left;
} else {
node = node.right;
}
}
return node;
}
/**
* 最小值
*
* @param node 根節點
* @return
*/
public Node treeMinimum(Node node) {
while (node.left != null) {
node = node.left;
}
return node;
}
/**
* 最大值
*
* @param node 根節點
* @return
*/
public Node treeMaximum(Node node) {
while (node.right != null) {
node = node.right;
}
return node;
}
/**
* 前驅
*
* @param node 根節點
* @return
*/
public Node treePredecessor(Node node) {
// 如果存在左子樹,返回左子樹的最大值
if (node.left != null) {
return treeMaximum(node.left);
}
Node y = node.parent;
// 當不存在左子樹時,返回最低祖先節點
while (y != null && node == y.left) {
node = y;
y = y.parent;
}
return y;
}
/**
* 後繼
*
* @param node 根節點
* @return
*/
public Node treeSuccessor(Node node) {
// 如果存在右子樹,返回右子樹的最小值
if (node.right != null) {
return treeMinimum(node.right);
}
Node y = node.parent;
// 當不存在右子樹時,返回最低祖先節點
while (y != null && node == y.right) {
node = y;
y = y.parent;
}
return y;
}
/**
* 左旋(node節點必有右孩子)
*
* @param node
*/
public void leftTotate(Node node) {
Node y = node.right;
node.right = y.left;
if (y.left != null)
y.left.parent = node;
y.parent = node.parent;
if (node.parent == null) {
root = y;
} else if (node == node.parent.left) {
node.parent.left = y;
} else {
node.parent.right = y;
}
node.parent = y;
y.left = node;
}
/**
* 右旋(node節點必有左孩子)
*
* @param node
*/
public void rightTotate(Node node) {
Node y = node.left;
node.left = y.right;
if (y.right != null)
y.right.parent = node;
y.parent = node.parent;
if (node.parent == null) {
root = y;
} else if (node == node.parent.left) {
node.parent.left = y;
} else {
node.parent.right = y;
}
node.parent = y;
y.right = node;
}
/**
* 插入
*
* @param key 插入節點的關鍵值
*/
public void RBTreeInsert(int key) {
// 建立插入節點
Node node = new Node(key);
// 定義插入節點的父節點變數
Node y = null;
// 定義臨時變數存根節點
Node x = root;
// 在根節點的左、右子樹中查詢插入位置
while (x != null) {
y = x;
if (key < x.key) {
x = x.left;
} else {
x = x.right;
}
}
node.parent = y;
if (y == null) {
root = node;
} else if (key < y.key) {
y.left = node;
} else {
y.right = node;
}
RBTreeInsertFixup(node);
}
/**
* 插入後修復
*
* @param node 插入節點
*/
public void RBTreeInsertFixup(Node node) {
// 當插入節點的父節點為紅色時,執行迴圈
while (node.parent != null && !node.parent.color && node.parent.parent != null) {
// 當插入節點的父節點為其爺爺節點的左孩子時
if (node.parent == node.parent.parent.left) {
// 定義y存叔叔節點
Node y = node.parent.parent.right;
// 如果叔叔節點為紅色,將父節點與叔叔節點變成黑色,爺爺節點變成紅色,將插入節點升級為爺爺節點
if (y != null && !y.color) {
node.parent.color = true;
y.color = true;
node.parent.parent.color = false;
node = node.parent.parent;
} else if (node == node.parent.right) {//如果叔叔節點為黑色,插入節點是父節點的右孩子,將插入節點升級為父節點,左旋插入節點
node = node.parent;
leftTotate(node);
} else {//如果叔叔節點為黑色,插入節點是父節點的左孩子,將父節點變成黑色,爺爺節點變成紅色,右旋爺爺節點
node.parent.color = true;
node.parent.parent.color = false;
rightTotate(node.parent.parent);
}
} else {// 當插入節點的父節點為其爺爺節點的右孩子時
// 定義y存叔叔節點
Node y = node.parent.parent.left;
// 如果叔叔節點為紅色,將父節點與叔叔節點變成黑色,爺爺節點變成紅色,將插入節點升級為爺爺節點
if (y != null && !y.color) {
node.parent.color = true;
y.color = true;
node.parent.parent.color = false;
node = node.parent.parent;
} else if (node == node.parent.left) {//如果叔叔節點為黑色,插入節點是父節點的左孩子,將插入節點升級為父節點,右旋插入節點
node = node.parent;
rightTotate(node);
} else {//如果叔叔節點為黑色,插入節點是父節點的右孩子,將父節點變成黑色,爺爺節點變成紅色,左旋爺爺節點
node.parent.color = true;
node.parent.parent.color = false;
leftTotate(node.parent.parent);
}
}
}
// 將根節點變成黑色
if (root.parent != null) {
root = root.parent;
}
root.color = true;
}
/**
* 刪除
*
* @param node 刪除節點
* @return
*/
public Node RBTreeDelete(Node node) {
// 定義臨時變數存刪除節點或後繼節點
Node y;
// 當刪除節點至多有一個孩子時
if (node.left == null || node.right == null) {
y = node;
} else {// 當刪除節點有兩個孩子時,y存後繼節點
y = treeSuccessor(node);
}
// 定義臨時變數存刪除節點的孩子節點
Node x;
if (y.left != null) {
x = y.left;
} else {
x = y.right;
}
if (x != null) {
x.parent = y.parent;
} else {
x = nil;
x.parent = y.parent;
}
if (y.parent == null) {
root = x;
} else if (y == y.parent.left) {
y.parent.left = x;
} else {
y.parent.right = x;
}
// 當y為後繼節點時,將y的關鍵值賦給刪除節點
if (y != node) {
node.key = y.key;
}
// 當y為黑色時,需要修復紅黑樹
if (y.color) {
RBTreeDeleteFixup(x);
}
return y;
}
/**
* 刪除後修復
*
* @param node 刪除節點的孩子節點
*/
public void RBTreeDeleteFixup(Node node) {
// 當node不等於根節點並且為黑色時,執行迴圈
while (node != root && (node == nil || node.color)) {
// 如果node節點為父節點的左孩子
if (node == node.parent.left) {
// 定義w存兄弟節點
Node w = node.parent.right;
// 當兄弟節點為紅色時,將兄弟節點變成黑色,父節點變成紅色,左旋父節點,更新兄弟節點
if (!w.color) {
w.color = true;
node.parent.color = false;
leftTotate(node.parent);
w = node.parent.right;
} else if (w.left.color && w.right.color) {//當兄弟節點為黑色且其兩個孩子都為黑色時,將兄弟節點變成紅色,將node節點升級為父節點
w.color = false;
node = node.parent;
} else if (w.right.color) {//當兄弟節點為黑色且其左孩子為紅色、其右孩子為黑色時,將其左孩子變成黑色、兄弟節點變成紅色,右旋兄弟節點,更新兄弟節點
w.left.color = true;
w.color = false;
rightTotate(w);
w = node.parent.right;
} else {//當兄弟節點為黑色且其右孩子為紅色時,將父節點的顏色賦給兄弟節點,父節點變成黑色,兄弟節點的右孩子變成黑色,左旋父節點
w.color = node.parent.color;
node.parent.color = true;
w.right.color = true;
leftTotate(node.parent);
// 將根節點賦給node
if (root.parent != null) {
root = root.parent;
}
node = root;
}
} else {// 如果node節點為父節點的右孩子
// 定義w存兄弟節點
Node w = node.parent.left;
// 當兄弟節點為紅色時,將兄弟節點變成黑色,父節點變成紅色,右旋父節點,更新兄弟節點
if (!w.color) {
w.color = true;
node.parent.color = false;
rightTotate(node.parent);
w = node.parent.left;
} else if (w.left.color && w.right.color) {//當兄弟節點為黑色且其兩個孩子都為黑色時,將兄弟節點變成紅色,將node節點升級為父節點
w.color = false;
node = node.parent;
} else if (w.left.color) {//當兄弟節點為黑色且其左孩子為黑色、其右孩子為紅色時,將其右孩子變成黑色、兄弟節點變成紅色,左旋兄弟節點,更新兄弟節點
w.right.color = true;
w.color = false;
leftTotate(w);
w = node.parent.left;
} else {//當兄弟節點為黑色且其左孩子為紅色時,將父節點的顏色賦給兄弟節點,父節點變成黑色,兄弟節點的左孩子變成黑色,右旋父節點
w.color = node.parent.color;
node.parent.color = true;
w.left.color = true;
rightTotate(node.parent);
// 將根節點賦給node
if (root.parent != null) {
root = root.parent;
}
node = root;
}
}
}
// 將node節點變成黑色
node.color = true;
}
public static void main(String[] args) {
int[] arr = {21, 3, 6, 7, 12, 25, 17, 8, 15};
RBTree rb = new RBTree(21);
for (int i = 1; i < arr.length; i++) {
rb.RBTreeInsert(arr[i]);
}
rb.inOrderTreeWalk(root);
rb.RBTreeDelete(rb.treeSearch(root, 21));
System.out.println();
rb.inOrderTreeWalk(root);
}
}
轉載連結1:https://adbycool.blog.csdn.net/article/details/108505979