紅黑樹和AVL樹的思想是相似的,都是在插入過程當中對二叉排序樹進行調整,從而提高性能,它的增刪改查都可以在**O(lg n)**內完成。git
本文會從定義到實現一棵紅黑樹展開,還會簡單介紹其與AVL樹的異同。github
紅黑樹是一棵二叉排序樹。且知足如下特色:編程
下圖就是一棵簡單的紅黑樹示例: 數組
示例中每一個結點最後都是一個NIL結點,它是黑色的,不過咱們畫圖時一般會省略它。因此下文以及後續文章中繪製時都會省略NIL結點,你們記得還有它就能夠。微信
紅黑樹的插入與刪除和AVL樹相似,也是每插入一個結點,都檢查是否破壞了樹的結構,而後進行調整。紅黑樹每一個結點插入時默認都爲紅色,這樣作能夠下降黑高,也能夠減小調整的次數。數據結構
紅黑樹的概念理解起來較爲複雜,咱們以一個簡單的示例,看看如何構造一棵紅黑樹。源碼分析
現有數組int[] a = {1, 10, 9, 2, 3, 8, 7, 4, 5, 6};
咱們要將其變爲一棵紅黑樹。性能
首先插入1,此時樹是空的,1就是根結點,根結點是黑色的:3d
而後插入元素10,此時依然符合規則,結果以下:code
當插入元素9時,這時是須要調整的第一種狀況,結果以下:
紅黑樹規則4中強調不能有兩個相鄰的紅色結點,因此此時咱們須要對其進行調整。調整的原則有多個相關因素,這裏的狀況是,父結點10是其祖父結點1(父結點的父結點)的右孩子,當前結點9是其父結點10的左孩子,且沒有叔叔結點(父結點的兄弟結點),此時須要進行兩次旋轉,第一次,以父結點10右旋:
而後將父結點**(此時是9)染爲黑色,祖父結點1**染爲紅色,以下所示:
而後以祖父結點1左旋:
下一步,插入元素2,結果以下:
此時狀況與上一步相似,區別在於父結點1是祖父結點9的左孩子,當前結點2是父結點的右孩子,且叔叔結點10是紅色的。這時須要先將叔叔結點10染爲黑色,再進行下一步操做,具體作法是將父結點1和叔叔結點10染爲黑色,祖父結點9染爲紅色,以下所示:
因爲結點9是根節點,必須爲黑色,將它染爲黑色便可:
下一步,插入元素3,以下所示:
這和咱們以前插入元素10的狀況如出一轍,須要將父結點2染爲黑色,祖父結點1染爲紅色,以下所示:
而後左旋:
下一步,插入元素8,結果以下:
此時和插入元素2有些相似,區別在於父結點3是右孩子,當前結點8也是右孩子,這時也須要先將叔叔結點1染爲黑色,具體操做是先將1和3染爲黑色,再將祖父結點2染爲紅色,以下所示:
此時樹已經平衡了,不須要再進行其餘操做了,如今插入元素7,以下所示:
這時和以前插入元素9時如出一轍了,先將7和8右旋,以下所示:
而後將7染爲黑色,3染爲紅色,再進行左旋,結果以下:
下一步要插入的元素是4,結果以下:
這裏和插入元素2是相似的,先將3和8染爲黑色,7染爲紅色,以下所示:
但此時2和7相鄰且顏色均爲紅色,咱們須要對它們繼續進行調整。這時狀況變爲了父結點2爲紅色,叔叔結點10爲黑色,且2爲左孩子,7爲右孩子,這時須要以2左旋。這時左旋與以前不一樣的地方在於結點7旋轉完成後將有三個孩子,結果相似於下圖:
這種狀況處理起來也很簡單,只須要把7原來的左孩子3,變成2的右孩子便可,結果以下:
而後再把2的父結點7染爲黑色,祖父結點9染爲紅色。結果以下所示:
此時又須要右旋了,咱們要以9右旋,右旋完成後7又有三個孩子,這種狀況和上述是對稱的,咱們把7原有的右孩子8,變成9的左孩子便可,以下所示:
下一個要插入的元素是5,插入後以下所示:
有了上述一些操做,處理5變得十分簡單,將3染爲紅色,4染爲黑色,而後左旋,結果以下所示:
最後插入元素6,以下所示:
又是叔叔結點3爲紅色的狀況,這種狀況咱們處理過屢次了,首先將3和5染爲黑色,4染爲紅色,結果以下:
此時問題向上傳遞到了元素4,咱們看2、4、7、9的顏色和位置關係,這種狀況咱們也處理過,先將2和9染爲黑色,7染爲紅色,結果以下:
最後7是根結點,染爲黑色便可,最終結果以下所示:
能夠看到,在插入元素時,叔叔結點是主要影響因素,待插入結點與父結點的關係決定了是否須要屢次旋轉。能夠總結爲如下幾種狀況:
若是父結點是黑色,插入便可,無需調整。
若是叔叔結點是紅色,就把父結點和叔叔結點都轉爲黑色,祖父結點轉爲紅色,將不平衡向上傳遞。
若是叔叔結點是黑色或者沒有叔叔結點,就看父結點和待插入結點的關係。若是待插入結點和父結點的關係,與父結點與祖父結點的關係一致,好比待插入結點是父結點的左孩子,父結點也是祖父結點的左孩子,就無需屢次旋轉。不然就先經過相應的旋轉將其關係變爲一致。
要從一棵紅黑樹中刪除一個元素,主要分爲三種狀況。
沒有孩子指的是沒有值不爲NIL的孩子。這種狀況下,若是刪除的元素是紅色的,能夠直接刪除,若是刪除的元素是黑色的,就須要進行調整了。
例如咱們從下圖中刪除元素1:
刪除元素1後,2的左孩子爲NIL,這條支路上的黑色結點數就比其餘支路少了,因此須要進行調整。
這時,咱們的關注點從叔叔結點轉到兄弟結點,也就是結點4,此時4是紅色的,就把它染爲黑色,把父結點2染爲紅色,以下所示:
而後以2左旋,結果以下:
此時兄弟結點爲3,且它沒有紅色的孩子,這時只須要把它染爲紅色,父結點2染爲黑色便可。結果以下所示:
這應該是刪除操做中最簡單的一種狀況了,根據紅黑樹的定義,咱們能夠推測,若是一個元素僅有一個孩子,那麼這個元素必定是黑色的,並且其孩子是紅色的。
假設咱們有一個紅色節點,它是樹中的某一個節點,且僅有一個孩子,那麼根據紅色節點不能相鄰的條件,它的孩子必定是黑色的,以下所示:
但這個子樹的黑高卻再也不平衡了(注意每一個節點的葉節點都是一個NIL節點),所以紅色節點不可能只有一個孩子。
而如果一個黑色節點僅有一個孩子,若是其孩子是黑色的,一樣會打破黑高的平衡,因此其孩子只能是紅色的,以下所示:
只有這一種狀況符合紅黑樹的定義,這時要刪除這個元素,只須要使用其孩子代替它,僅代替值而不代替顏色便可,上圖的狀況刪除完後變爲:
能夠看到,樹的黑高並無發生變化,所以也不須要進行調整。
咱們在討論二叉排序樹時說過,若是刪除一個有兩個孩子的元素,可使用它的前驅或者後繼結點代替它。由於它的前驅或者後繼結點最多隻會有一個孩子,因此這種狀況能夠轉爲狀況1或狀況2處理。
刪除元素最複雜的是狀況1,這主要由其兄弟結點以及兄弟結點的孩子顏色共同決定。這裏簡要作下總結。
咱們以N表明當前待刪除節點,以P表明父結點,以S表明兄弟結點,以SL表明兄弟結點的左孩子,SR表明兄弟結點的右孩子,以下所示:
根據紅黑樹定義,這種狀況下S要麼有紅色的子結點,要麼只有NIL結點,如下對S有黑色結點的狀況均表示NIL
主要有如下幾種:
此時把P和S顏色變換,再左旋,以下:
這樣變換後,N支路上的黑色結點並無增長,因此依然少一個,
不管S有幾個孩子,或者沒有孩子,只要不是紅色都是這種狀況,此時狀況以下:
咱們把S染爲紅色,這樣一來,N和S兩個支路都少了一個黑色結點,因此能夠把問題向父結點轉移,經過遞歸解決。染色後以下:
這種狀況最爲簡單,只須要把P和S顏色交換便可。這樣N支路多了一個黑色元素,而S支路沒有減小,因此達到了平衡。
以下所示
此時將S改成P的顏色,SR和P改成黑色,而後左旋,結果以下:
能夠發現,此時N支路多了一個黑色結點,而其他支路均沒有收到影響,因此調整完畢。
此時變換S和SL的顏色,而後右旋,結果以下:
這時,全部分支的黑色結點數均沒有改變,但狀況5轉爲了狀況4,再進行一次操做便可。
還有一些狀況與上述是對稱的,咱們進行相應的轉換便可。
#總結 紅黑樹的操做比較複雜,插入元素可能須要屢次變色與旋轉,刪除也是。這些操做的目的都是爲了保證紅黑樹的結構不被破壞。這些複雜的插入與刪除操做但願你們能夠親手嘗試一下,以加深理解。
紅黑樹是JDK中TreeMap、TreeSet的底層數據結構,在JDK1.8中HashMap也用到了紅黑樹,因此掌握它對咱們後續的分析十分重要。
關於紅黑樹與AVL樹的區別,以及爲什麼選用紅黑樹,已經不屬於咱們的討論範圍,你們能夠查閱相關資料進一步瞭解。
上一篇:Java集合源碼分析之基礎(五):平衡二叉樹(AVL Tree)
本文到此就結束了,若是您喜歡個人文章,能夠關注個人微信公衆號: 大大紙飛機
或者掃描下方二維碼直接添加:
您也能夠關注個人github:github.com/LtLei/artic…
編程之路,道阻且長。惟,路漫漫其修遠兮,吾將上下而求索。