30張圖帶你徹底理解紅黑樹
寫在前面
當在10億數據中只需要進行10幾次比較就能查找到目標時,不禁感嘆編程之魅力!人類之偉大呀! —— 學紅黑樹有感。
終於,在學習了幾天的紅黑樹相關的知識后,我想把我所學所想和所感分享給大家。紅黑樹是一種比較難的數據結構,要完全搞懂非常耗時耗力,紅黑樹怎么自平衡?什么時候需要左旋或右旋?插入和刪除破壞了樹的平衡后怎么處理?等等一連串的問題在學習前困擾着我。如果你在學習過程中也會存在我的疑問,那么本文對你會有幫助,本文幫助你全面、徹底地理解紅黑樹!
本文將通過圖文的方式講解紅黑樹的知識點,並且不會涉及到任何代碼,相信我,在懂得紅黑樹實現原理前,看代碼會一頭霧水的,當原理懂了,代碼也就按部就班寫而已,沒任何難度。
閱讀本文你需具備知識點:
- 二叉查找樹
- 完美平衡二叉樹
事不宜遲,讓我們進入正題吧。
正文
紅黑樹也是二叉查找樹,我們知道,二叉查找樹這一數據結構並不難,而紅黑樹之所以難是難在它是自平衡的二叉查找樹,在進行插入和刪除等可能會破壞樹的平衡的操作時,需要重新自處理達到平衡狀態。現在在腦海想下怎么實現?是不是太多情景需要考慮了?嘖嘖,先別急,通過本文的學習后,你會覺得,其實也不過如此而已。好吧,我們先來看下紅黑樹的定義和一些基本性質。
紅黑樹定義和性質
紅黑樹是一種含有紅黑結點並能自平衡的二叉查找樹。它必須滿足下面性質:
- 性質1:每個節點要么是黑色,要么是紅色。
- 性質2:根節點是黑色。
- 性質3:每個葉子節點(NIL)是黑色。
- 性質4:每個紅色結點的兩個子結點一定都是黑色。
- 性質5:任意一結點到每個葉子結點的路徑都包含數量相同的黑結點。
從性質5又可以推出:
- 性質5.1:如果一個結點存在黑子結點,那么該結點肯定有兩個子結點-
圖1就是一顆簡單的紅黑樹。其中Nil為葉子結點,並且它是黑色的。(值得提醒注意的是,在Java中,葉子結點是為null的結點。)
紅黑樹並不是一個完美平衡二叉查找樹,從圖1可以看到,根結點P的左子樹顯然比右子樹高,但左子樹和右子樹的黑結點的層數是相等的,也即任意一個結點到到每個葉子結點的路徑都包含數量相同的黑結點(性質5)。所以我們叫紅黑樹這種平衡為黑色完美平衡。
介紹到此,為了后面講解不至於混淆,我們還需要來約定下紅黑樹一些結點的叫法,如圖2所示。
我們把正在處理(遍歷)的結點叫做當前結點,如圖2中的D,它的父親叫做父結點,它的父親的另外一個子結點叫做兄弟結點,父親的父親叫做祖父結點。
前面講到紅黑樹能自平衡,它靠的是什么?三種操作:左旋、右旋和變色。
- 左旋:以某個結點作為支點(旋轉結點),其右子結點變為旋轉結點的父結點,右子結點的左子結點變為旋轉結點的右子結點,左子結點保持不變。如圖3。
- 右旋:以某個結點作為支點(旋轉結點),其左子結點變為旋轉結點的父結點,左子結點的右子結點變為旋轉結點的左子結點,右子結點保持不變。如圖4。
- 變色:結點的顏色由紅變黑或由黑變紅。
上面所說的旋轉結點也即旋轉的支點,圖4和圖5中的P結點。
我們先忽略顏色,可以看到旋轉操作不會影響旋轉結點的父結點,父結點以上的結構還是保持不變的。
左旋只影響旋轉結點和其右子樹的結構,把右子樹的結點往左子樹挪了。
右旋只影響旋轉結點和其左子樹的結構,把左子樹的結點往右子樹挪了。
以旋轉操作是局部的。另外可以看出旋轉能保持紅黑樹平衡的一些端詳了:當一邊子樹的結點少了,那么向另外一邊子樹“借”一些結點;當一邊子樹的結點多了,那么向另外一邊子樹“租”一些結點。
但要保持紅黑樹的性質,結點不能亂挪,還得靠變色了。怎么變?具體情景又不同變法,后面會具體講到,現在只需要記住紅黑樹總是通過旋轉和變色達到自平衡。
balabala了這么多,相信你對紅黑樹有一定印象了,那么現在來考考你:
思考題1:黑結點可以同時包含一個紅子結點和一個黑子結點嗎? (答案見文末)
接下來先講解紅黑樹的查找熱熱身。
紅黑樹查找
因為紅黑樹是一顆二叉平衡樹,並且查找不會破壞樹的平衡,所以查找跟二叉平衡樹的查找無異:
- 從根結點開始查找,把根結點設置為當前結點;
- 若當前結點為空,返回null;
- 若當前結點不為空,用當前結點的key跟查找key作比較;
- 若當前結點key等於查找key,那么該key就是查找目標,返回當前結點;
- 若當前結點key大於查找key,把當前結點的左子結點設置為當前結點,重復步驟2;
- 若當前結點key小於查找key,把當前結點的右子結點設置為當前結點,重復步驟2;
如圖5所示。
非常簡單,但簡單不代表它效率不好。正由於紅黑樹總保持黑色完美平衡,所以它的查找最壞時間復雜度為O(2lgN),也即整顆樹剛好紅黑相隔的時候。能有這么好的查找效率得益於紅黑樹自平衡的特性,而這背后的付出,紅黑樹的插入操作功不可沒~
紅黑樹插入
插入操作包括兩部分工作:一查找插入的位置;二插入后自平衡。查找插入的父結點很簡單,跟查找操作區別不大:
- 從根結點開始查找;
- 若根結點為空,那么插入結點作為根結點,結束。
- 若根結點不為空,那么把根結點作為當前結點;
- 若當前結點為null,返回當前結點的父結點,結束。
- 若當前結點key等於查找key,那么該key所在結點就是插入結點,更新結點的值,結束。
- 若當前結點key大於查找key,把當前結點的左子結點設置為當前結點,重復步驟4;
- 若當前結點key小於查找key,把當前結點的右子結點設置為當前結點,重復步驟4;
如圖6:
ok,插入位置已經找到,把插入結點放到正確的位置就可以啦,但插入結點是應該是什么顏色呢?答案是紅色。 理由很簡單,紅色在父結點(如果存在)為黑色結點時,紅黑樹的黑色平衡沒被破壞,不需要做自平衡操作。但如果插入結點是黑色,那么插入位置所在的子樹黑色結點總是多1,必須做自平衡。
所有插入情景如圖7所示。
嗯,插入情景很多呢,8種插入情景!但情景1、2和3的處理很簡單,而情景4.2和情景4.3只是方向反轉而已,懂得了一種情景就能推出另外一種情景,所以總體來看,並不復雜,后續我們將一個一個情景來看,把它徹底搞懂。
另外,根據二叉樹的性質,除了情景2,所有插入操作都是在葉子結點進行的。這點應該不難理解,因為查找插入位置時,我們就是在找子結點為空的父結點的。
在開始每個情景的講解前,我們還是先來約定下,如圖8所示。
圖8的字母並不代表結點Key的大小。I表示插入結點,P表示插入結點的父結點,S表示插入結點的叔叔結點,PP表示插入結點的祖父結點。
好了,下面讓我們一個一個來分析每個插入的情景以其處理。
插入情景1:紅黑樹為空樹
最簡單的一種情景,直接把插入結點作為根結點就行,但注意,根據紅黑樹性質2:根節點是黑色。還需要把插入結點設為黑色。
處理:把插入結點作為根結點,並把結點設置為黑色。
插入情景2:插入結點的Key已存在
插入結點的Key已存在,既然紅黑樹總保持平衡,在插入前紅黑樹已經是平衡的,那么把插入結點設置為將要替代結點的顏色,再把結點的值更新就完成插入。
處理:
- 把I設為當前結點的顏色
- 更新當前結點的值為插入結點的值
插入情景3:插入結點的父結點為黑結點
由於插入的結點是紅色的,當插入結點的父結點為黑色時,並不會影響紅黑樹的平衡,直接插入即可,無需做自平衡。
處理:直接插入。
插入情景4:插入結點的父結點為紅結點
再次回想下紅黑樹的性質2:**根結點是黑色。如果插入的父結點為紅結點,那么該父結點不可能為根結點,所以插入結點總是存在祖父結點。**這點很重要,因為后續的旋轉操作肯定需要祖父結點的參與。
情景4又分為很多子情景,下面將進入重點部分,各位看官請留神了。
插入情景4.1:叔叔結點存在並且為紅結點
從紅黑樹性質4可以,祖父結點肯定為黑結點,因為不可以同時存在兩個相連的紅結點。那么此時該插入子樹的紅黑層數的情況是:黑紅紅。顯然最簡單的處理方式是把其改為:紅黑紅。如圖9和圖10所示。
處理:
1.將P和S設置為黑色
2.將PP設置為紅色
3.把PP設置為當前插入結點 (查看是否需要繼續處理)
可以看到,我們把PP結點設為紅色了,如果PP的父結點是黑色,那么無需再做任何處理;但如果PP的父結點是紅色,根據性質4,此時紅黑樹已不平衡了,所以還需要把PP當作新的插入結點,繼續做插入操作自平衡處理,直到平衡為止。
試想下PP剛好為根結點時,那么根據性質2,我們必須把PP重新設為黑色,那么樹的紅黑結構變為:黑黑紅。換句話說,從根結點到葉子結點的路徑中,黑色結點增加了**。這也是唯一一種會增加紅黑樹黑色結點層數的插入情景。**
我們還可以總結出另外一個經驗:紅黑樹的生長是自底向上的。這點不同於普通的二叉查找樹,普通的二叉查找樹的生長是自頂向下的。
插入情景4.2:叔叔結點不存在或為黑結點,並且插入結點的父親結點是祖父結點的左子結點
單純從插入前來看,也即不算情景4.1自底向上處理時的情況,叔叔結點非紅即為葉子結點(Nil)。因為如果叔叔結點為黑結點,而父結點為紅結點,那么叔叔結點所在的子樹的黑色結點就比父結點所在子樹的多了,這不滿足紅黑樹的性質5。后續情景同樣如此,不再多做說明了。
前文說了,需要旋轉操作時,肯定一邊子樹的結點多了或少了,需要租或借給另一邊。插入顯然是多的情況,那么把多的結點租給另一邊子樹就可以了。
插入情景4.2.1:插入結點是其父結點的左子結點
處理:
1.將P設為黑色
2.將PP設為紅色
3.對PP進行右旋
由圖11可得,左邊兩個紅結點,右邊不存在,那么一邊一個剛剛好,並且因為為紅色,肯定不會破壞樹的平衡。
咦,可以把P設為紅色,I和PP設為黑色嗎?答案是可以!看過《算法:第4版》的同學可能知道,書中講解的就是把P設為紅色,I和PP設為黑色。但把P設為紅色,顯然又會出現情景4.1的情況,需要自底向上處理,做多了無謂的操作,既然能自己消化就不要麻煩祖輩們啦~
插入情景4.2.2:插入結點是其父結點的右子結點
這種情景顯然可以轉換為情景4.2.1,如圖12所示,不做過多說明了。
處理:
1.對P進行左旋
2.把P設置為插入結點,得到情景4.2.1
3.進行情景4.2.1的處理
插入情景4.3:叔叔結點不存在或為黑結點,並且插入結點的父親結點是祖父結點的右子結點
該情景對應情景4.2,只是方向反轉,不做過多說明了,直接看圖。
插入情景4.3.1:插入結點是其父結點的右子結點
處理:
1.將P設為黑色
2.將PP設為紅色
3,對PP進行左旋
插入情景4.3.2:插入結點是其父結點的右子結點
處理:
1.對P進行右旋
2.把P設置為插入結點,得到情景4.3.1
3. 進行情景4.3.1的處理
好了,講完插入的所有情景了。可能又同學會想:上面的情景舉例的都是第一次插入而不包含自底向上處理的情況,那么上面所說的情景都適合自底向上的情況嗎?答案是肯定的。理由很簡單,但每棵子樹都能自平衡,那么整棵樹最終總是平衡的。好吧,在出個習題,請大家拿出筆和紙畫下試試(請務必動手畫下,加深印象):