普通的二叉搜索樹在最壞的情況下,可能退化成一個鏈表。而又因為二叉搜索樹的所有操作的性能(添加,刪除,查找等),與二叉搜索樹的高度有關。在最壞的情況下,二叉搜索樹的高度和元素個數相同,此時二叉搜索樹的效率降為了O(n)級別。
所以為了防止我們的二叉搜索樹退化成一個鏈表,就產生了平衡二叉樹。平衡二叉樹可以保證它的左右兩個子樹的高度差不會超過1。平衡二叉樹有很多實現,一個經典實現就是紅黑樹。
紅黑樹
在紅黑樹中將樹中的節點劃分為兩種狀態,分別用黑色和紅色來表示。
紅黑樹為了保證自己能夠平衡子樹,所以制訂以下五個規則:
1、每個節點必須有顏色,要么黑色,要么紅色,沒有別的顏色。
2、根節點必須是黑色
3、所有的空節點(nil節點)都認為是黑色節點。
4、紅色的節點不能連續,即一個紅色的節點,它的父節點和子節點不能也是紅色的,
5、無論從哪一個節點起始,到它每個葉子節點的路徑中,黑色節點數量必須相同。
在對紅黑樹進行添加、刪除等操作之后,必須使紅黑樹符合這5個規則。
那么問題來了,在添加刪除操作之后,樹中節點的數量都變了,是怎么保證整個樹滿足上述這些規則呢?
這里涉及到3種操作,變色、左旋和右旋。通過這個三種操作,在增刪節點之后調整樹的形狀結構,使它滿足上述5個規則。這也是紅黑樹能保持平衡的原因。
變色操作我們在下文的添加、刪除節點的實際操作中,再進行在描述。
先來說一下左右旋。
左旋
左旋是指以某節點為支點,進行逆時針旋轉。如下圖,是以2為支點進行的左旋:文字描述一下就是,2的右孩子節點4,變為了2的父節點,2由父節點變為4的左孩子。同時,4原來的左孩子變為2的右孩子。
右旋
右旋與左旋相反,即以某節點為支點進行順時針旋轉。同樣,我們看下圖,是以5為支點進行的右旋:
文字描述同樣反過來,5的左孩子節點3,變為5的父節點,5由父節點變為3的右孩子。3原來的右孩子變為5的左孩子。
插入新節點
首先是在樹中找到新節點正確的位置,尋找位置的過程與普通的二叉搜索樹相同,只是將新插入的節點默認為紅色節點。為什么默認為紅色?因為如果你將新節點默認為黑色,則插入后肯定會打破原本符合規則的紅黑樹(上述第5條規則)。但是,如果你將新節點定為紅色,則有可能不用任何操作就符合紅黑樹規則,如下圖,當新插入的紅色節點,它的父親節點為黑色時候,此時已經滿足紅黑樹規則了。所以用紅色比黑色好。
如果很不巧,新插入的節點的父親節點也為紅色,因為紅色節點不能連續,所以我們需要調整紅黑樹的結構使其滿足規則。在調整的過程中我們會遇到3種需要處理的情況,我們來一一進行說明。
情況1:
插入新節點40,此時它的父節點為紅色,并且它的叔叔節點(即51)也為紅色。此時我們需要進行變色操作。將該節點的父親節點、叔叔節點都變為黑色,祖父節點變為紅色。
此時上圖已經滿足紅黑樹的規則。但有的時候我們經過了變色操作后,仍不滿足紅黑樹的規則,會遇到下面的情況。
情況2:
如圖,我們插入新的節點53,在按情況1的操作變色后,變成了這樣:
此時,49與44為兩個連續的紅色,顯然不符合規則。此時的操作為:我們將49看做當前節點,將當前節點的父節點44變為黑色,祖父節點35置為紅色,以祖父節點為支點進行左旋。
此時情況2就處理完成了。
最后我們說一下情況3的情景,如下圖:
我們向樹中插入新節點37,在按情況1的操作變色后,變成了這樣:
情況3:
此時,49與44為兩個連續的紅色,顯然不符合規則。而我們只需以49為支點,進行一次右旋,就變成了情況2。如下圖。
再按情況2進行一次操作就符合規則了。
3種情況我們說明完了,但是你可能還會有這樣的疑問,什么時候進行左旋,什么時候進行右旋;什么時候以父節點為支點旋轉,什么時候又以祖父節點為支點旋轉?
那么我們可以總結一下,當遇到連續的紅色節點應該怎么辦:當前節點我們叫它X,如果X相對于父節點的左右位置和父節點相對于祖父節點的左右位置相同,此時,就以祖父節點為支點,進行反向旋轉。例如:X為父節點的左孩子,X的父節點同樣也是其祖父節點的左孩子,此時以祖父節點為支點進行右旋;
如果X相對于父節點的左右位置和父節點相對于祖父節點的左右位置不同,則以X的父節點為支點,進行旋轉,旋轉方向與X相對于父節點左右位置相反。例如:X為其父節點的左孩子,X的父節點為祖父節點的右孩子,此時以X的父節點為支點進行一次右旋。
刪除節點
在紅黑樹中刪除節點,肯定要涉及到要刪的這個節點是紅色的還是黑色的。刪除紅色比較簡單,我們先說一下刪除紅色節點。
刪除節點要考慮這個節點所處的位置,所以我們羅列一下紅色的節點所有可能的位置情況。
- 它是一個葉子節點。
- 它既有左子樹也有右子樹。
你可能會發現為什么少了一種情況?它不能只有左子樹或者只有右子樹嗎?我們可以看下圖:
很明顯,這四種情況都不符合紅黑樹的規則,所以根本不會出現這種情況。
而對于既有左子樹也有右子樹的情況。我們可以先和普通的二叉搜索樹的刪除操作一樣,將它與前驅或者后繼交換一下。它就又變成第一種情況——成為了一個葉子節點。所以我們只需考慮當它是葉子節點的情況。
很簡單,直接刪除紅色葉子節點。
接下來我們看一下當要刪除的節點是黑色的時候應該怎么辦。
同樣我們列一下節點位置可能的情況:
- 它是一個葉子節點。
- 它只有左子樹,或只有右子樹。
- 它既有左子樹也有右子樹。
第三種情況和刪除紅色節點時的處理方法一樣,可以轉換成第一種或第二種情況,所以我們只關心前兩種情況。
當要刪除的黑色節點只有一個子樹時:
它的左孩子或者右孩子一定是紅色的,因為如果是黑色的就不符合紅黑樹的規則了。
操作方法為:我們只需要將它的子節點變黑,然后代替它的位置就完成了。
最后我們看一下最難處理的一種情況。
要刪除的黑色節點是葉子節點時:
情況1:待刪除黑色節點20,它的兄弟節點為紅色。
此時的操作為,將兄弟節點和父節點顏色交換,即父親變紅,兄弟變黑。然后以父節點為支點進行左旋。(旋轉方向同樣是與待刪除節點的左右位置相同)
此時會變成了下述的情況4,再按情況4進行操作就可以了。
情況2:待刪除黑色節點20,它的兄弟節點為黑色,并且它擁有紅色的遠侄子節點,近侄子節點有沒有都可以。(侄子節點即兄弟節點的子節點,遠侄子節點就是,當前節點如果是其父節點的左孩子,那么它的遠侄子節點就是兄弟節點的右孩子,近侄子同理)
操作方法為:將遠侄子節點變黑,兄弟節點與父親節點互換顏色,最后以父節點為支點進行左旋。(為什么是左旋?因為待刪除的20是左孩子,我們要將左子樹長度拉長,將它沉下來,使它變成多余的節點好刪除它,如果它是右孩子,則進行右旋)
操作后如下圖就完成了。
情況3:待刪除黑色節點20,它的兄弟節點為黑色,但它沒有紅色的遠侄子節點(即nil點,記住,nil點算黑色),只有紅色的近侄子節點。
操作方法為:將兄弟節點與近侄子節點交換顏色,再以兄弟節點進行右旋。(旋轉方向很好記,因為此處旋轉的目的是為了創造遠侄子,近侄子節點是左節點,所以就只能右旋了,如果近侄子是右節點則進行左旋。)
操作后如下圖:
此時有了紅色的遠侄子,就滿足了情況2,再按情況2進行一次操作就完成了。
情況4:待刪除黑色節點20,它的兄弟節點為黑色,遠侄子、近侄子節點都沒有。(即兩個nil節點,nil節點算黑色)
操作方法為:將兄弟節點變為紅色,同時最關鍵的是,將當前節點20的父節點50,看做當前節點,繼續遞歸的進行這四種情況的判斷,直到當前節點為紅色,或者當前節點是根節點才停止。最后將當前節點變為黑色!!
我們將上圖紅黑樹按流程演示一下:
第一步按情況4操作,將55變紅。并將父節點50看做當前節點,繼續操作。
此時當前節點61為紅色,滿足停止遞歸條件,將61變為黑色,停止。整個操作完成。
此時有關紅黑樹的知識就說完了。
以上所有內容都為自己查閱資料學習理解之后手敲的。盡量得采用通俗易懂的描述和解釋讓讀者更明白。27張圖都是自己親自畫的,花費了四天才寫完,如果覺得寫的還可以,麻煩點亮喜歡支持一下,如果還是不懂,可以下方留言QQ等聯系方式,我親自告訴你。