先來看下算法導論對R-B Tree的介紹:算法
紅黑樹,一種二叉查找樹,但在每一個節點上增長一個存儲位表示節點的顏色,能夠是Red或Black。 經過對任何一條從根到葉子的路徑上各個節點着色方式的限制,紅黑樹確保沒有一條路徑會比其餘路徑長出倆倍,於是是接近平衡的。segmentfault
前面說了,紅黑樹,是一種二叉查找樹,既然是二叉查找樹,那麼它必知足二叉查找樹的通常性質。 下面,在具體介紹紅黑樹以前,我們先來了解下二叉查找樹的通常性質:spa
而紅黑樹,能保證在最壞狀況下,基本的動態幾何操做的時間均爲O(lgn)。.net
ok,咱們知道,紅黑樹上每一個節點內含五個域,color,key,left,right,p。若是相應的指針域沒有,則設爲NIL。設計
通常的,紅黑樹,知足如下性質,即只有知足如下所有性質的樹,咱們才稱之爲紅黑樹:指針
(注:上述第三、5點性質中所說的NULL節點,包括wikipedia.算法導論上所認爲的葉子節點即爲樹尾端的NIL指針,或者說NULL節點。然百度百科以及網上一些其它博文直接說的葉節點,則易引發誤會,因,此葉節點非子節點)code
以下圖所示,便是一顆紅黑樹(下圖引自wikipedia:http://t.cn/hgvH1l):blog
此圖忽略了葉子和根部的父節點。同時,上文中咱們所說的 "葉節點" 或"NULL節點",如上圖所示,它不包含數據而只充當樹在此結束的指示,這些節點在繪圖中常常被省略,望看到此文後的讀者朋友注意。 繼承
當咱們在對紅黑樹進行插入和刪除等操做時,對樹作了修改,那麼可能會違背紅黑樹的性質。圖片
爲了保持紅黑樹的性質,咱們能夠經過對樹進行旋轉,即修改樹種某些節點的顏色及指針結構,以達到對紅黑樹進行插入、刪除節點等操做時,紅黑樹依然能保持它特有的性質(如上文所述的,五點性質)。
樹的旋轉,分爲左旋和右旋,如下藉助圖來作形象的解釋和介紹:
如上圖所示:
當在某個節點pivot上,作左旋操做時,咱們假設它的右孩子y不是NIL[T],pivot能夠爲樹內任意左孩子而不是NIL[T]的節點。
左旋以pivot到y之間的鏈爲「支軸」進行,它使y成爲該孩子樹新的根,而y的左孩子b則成爲pivot的右孩子。
來看算法導論對此操做的算法實現(以x代替上述的pivot):
snippet_file_name="blog_20140214_1_7335413" code_snippet_id="187675" snippet_file_name="blog_20140214_1_7335413" name="code"} LEFT-ROTATE(T, x) 1 y ← right[x] ▹ Set y. 2 right[x] ← left[y] ▹ Turn y's left subtree into x's right subtree. 3 p[left[y]] ← x 4 p[y] ← p[x] ▹ Link x's parent to y. 5 if p[x] = nil[T] 6 then root[T] ← y 7 else if x = left[p[x]] 8 then left[p[x]] ← y 9 else right[p[x]] ← y 10 left[y] ← x ▹ Put x on y's left. 11 p[x] ← y
右旋與左旋差很少,再此不作詳細介紹。
對於樹的旋轉,能保持不變的只有原樹的搜索性質,而原樹的紅黑性質則不能保持,在紅黑樹的數據插入和刪除後可利用旋轉和顏色重塗來恢復樹的紅黑性質。
至於有些書如 STL源碼剖析有對雙旋的描述,其實雙旋只是單旋的兩次應用,並沒有新的內容,所以這裏就再也不介紹了,並且左右旋也是相互對稱的,只要理解其中一種旋轉就能夠了。
I、ok,接下來,我們來具體瞭解紅黑樹的插入操做。
向一棵含有n個節點的紅黑樹插入一個新節點的操做能夠在O(lgn)時間內完成。
算法導論:
snippet_file_name="blog_20140214_2_2777034" code_snippet_id="187675" snippet_file_name="blog_20140214_2_2777034" name="code"} RB-INSERT(T, z) 1 y ← nil[T] 2 x ← root[T] 3 while x ≠ nil[T] 4 do y ← x 5 if key[z] < key[x] 6 then x ← left[x] 7 else x ← right[x] 8 p[z] ← y 9 if y = nil[T] 10 then root[T] ← z 11 else if key[z] < key[y] 12 then left[y] ← z 13 else right[y] ← z 14 left[z] ← nil[T] 15 right[z] ← nil[T] 16 color[z] ← RED 17 RB-INSERT-FIXUP(T, z)
我們來具體分析下,此段代碼:
RB-INSERT(T, z),將z插入紅黑樹T 以內。
爲保證紅黑性質在插入操做後依然保持,上述代碼調用了一個輔助程序RB-INSERT-FIXUP來對節點進行從新着色,並旋轉。
14 left[z] ← nil[T] 15 right[z] ← nil[T] //保持正確的樹結構
第16行,將z着爲紅色,因爲將z着爲紅色可能會違背某一條紅黑樹的性質,
因此,在第17行,調用RB-INSERT-FIXUP(T,z)來保持紅黑樹的性質。
RB-INSERT-FIXUP(T, z),以下所示:
snippet_file_name="blog_20140214_3_410484" code_snippet_id="187675" snippet_file_name="blog_20140214_3_410484" name="code"} 1 while color[p[z]] = RED 2 do if p[z] = left[p[p[z]]] 3 then y ← right[p[p[z]]] 4 if color[y] = RED 5 then color[p[z]] ← BLACK ▹ Case 1 6 color[y] ← BLACK ▹ Case 1 7 color[p[p[z]]] ← RED ▹ Case 1 8 z ← p[p[z]] ▹ Case 1 9 else if z = right[p[z]] 10 then z ← p[z] ▹ Case 2 11 LEFT-ROTATE(T, z) ▹ Case 2 12 color[p[z]] ← BLACK ▹ Case 3 13 color[p[p[z]]] ← RED ▹ Case 3 14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 15 else (same as then clause with "right" and "left" exchanged) 16 color[root[T]] ← BLACK
ok,參考一網友的言論,用本身的語言,再來具體解剖下上述倆段代碼。
爲了保證闡述清晰,我再寫下紅黑樹的5個性質:
在對紅黑樹進行插入操做時,咱們通常老是插入紅色的節點,由於這樣能夠在插入過程當中儘可能避免對樹的調整。 那麼,咱們插入一個節點後,可能會使原樹的哪些性質改變列? 因爲,咱們是按照二叉樹的方式進行插入,所以元素的搜索性質不會改變。
若是插入的節點是根節點,性質2會被破壞,若是插入節點的父節點是紅色,則會破壞性質4。 所以,總而言之,插入一個紅色節點只會破壞性質2或性質4。 咱們的恢復策略很簡單,
注:如下狀況三、四、5與上述算法導論上的代碼RB-INSERT-FIXUP(T, z),相對應:
插入修復具體操做狀況
1) 狀況1:插入的是根節點。
原樹是空樹,此狀況只會違反性質2。
對策:直接把此節點塗爲黑色。
2) 狀況2:插入的節點的父節點是黑色。
此不會違反性質2和性質4,紅黑樹沒有被破壞。
對策:什麼也不作。
3) 狀況3:當前節點的父節點是紅色且祖父節點的另外一個子節點(叔叔節點)是紅色。
此時父節點的父節點必定存在,不然插入前就已不是紅黑樹。與此同時,又分爲父節點是祖父節點的左子仍是右子,對於對稱性,咱們只要解開一個方向就能夠了。 在此,咱們只考慮父節點爲祖父左子的狀況。 同時,還能夠分爲當前節點是其父節點的左子仍是右子,可是處理方式是同樣的。咱們將此歸爲同一類。
對策:將當前節點的父節點和叔叔節點塗黑,祖父節點塗紅,把當前節點指向祖父節點,重新的當前節點從新開始算法。
針對狀況3,變化前(圖片來源:saturnman)[當前節點爲4節點]:
變化後:
4) 狀況4:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的右子
對策:當前節點的父節點作爲新的當前節點,以新當前節點爲支點左旋。
以下圖所示,變化前[當前節點爲7節點]:
變化後:
5) 狀況5:當前節點的父節點是紅色,叔叔節點是黑色,當前節點是其父節點的左子
解法:父節點變爲黑色,祖父節點變爲紅色,在祖父節點爲支點右旋
以下圖所示[當前節點爲2節點]
變化後:
回顧:通過上面狀況三、狀況四、狀況5等3種插入修復狀況的操做示意圖,讀者自會發現,後面的狀況四、狀況5都是針對狀況3插入節點4之後,進行的一系列插入修復狀況操做,不過,指向當前節點N指針一直在變化。因此,你能夠想固然的認爲:整個下來,狀況三、四、5就是一個完整的插入修復狀況的操做流程。
II、ok,接下來,我們最後來了解,紅黑樹的刪除操做:
爲了保證如下的介紹與闡述清晰,我第三次重寫下紅黑樹的5個性質
相信,重述了3次,你應該有深入記憶了。:D
咱們刪除的節點的方法與常規二叉搜索樹中刪除節點的方法是同樣的,若是被刪除的節點不是有雙非空子女,則直接刪除這個節點,用它的惟一子節點頂替它的位置,若是它的子節點分是空節點,那就用空節點頂替它的位置,若是它的雙子全爲非空,咱們就把它的直接後繼節點內容複製到它的位置,以後以一樣的方式刪除它的後繼節點,它的後繼節點不多是雙子非空,所以此傳遞過程最多隻進行一次。
繼續講解以前,補充說明下二叉樹節點刪除的幾種狀況,待刪除的節點按照兒子的個數能夠分爲三種:
沒有兒子,即爲葉節點。直接把父節點的對應兒子指針設爲NULL,刪除兒子節點就OK了。
只有一個兒子。那麼把父節點的相應兒子指針指向兒子的獨生子,刪除兒子節點也OK了。
有兩個兒子。這是最麻煩的狀況,由於你刪除節點以後,還要保證知足搜索二叉樹的結構。其實也比較容易,咱們能夠選擇左兒子中的最大元素或者右兒子中的最小元素放到待刪除節點的位置,就能夠保證結構的不變。固然,你要記得調整子樹,畢竟又出現了節點刪除。習慣上你們選擇左兒子中的最大元素,其實選擇右兒子的最小元素也同樣,沒有任何差異,只是人們習慣從左向右。這裏我們也選擇左兒子的最大元素,將它放到待刪節點的位置。左兒子的最大元素其實很好找,只要順着左兒子不斷的去搜索右子樹就能夠了,直到找到一個沒有右子樹的節點。那就是最大的了。
OK,回到紅黑樹上來。算法導論一書,給的紅黑樹節點刪除的算法實現是:
RB-DELETE(T, z) 單純刪除節點的總操做
snippet_file_name="blog_20140214_4_6299828" code_snippet_id="187675" snippet_file_name="blog_20140214_4_6299828" name="code"} 1 if left[z] = nil[T] or right[z] = nil[T] 2 then y ← z 3 else y ← TREE-SUCCESSOR(z) 4 if left[y] ≠ nil[T] 5 then x ← left[y] 6 else x ← right[y] 7 p[x] ← p[y] 8 if p[y] = nil[T] 9 then root[T] ← x 10 else if y = left[p[y]] 11 then left[p[y]] ← x 12 else right[p[y]] ← x 13 if y 3≠ z 14 then key[z] ← key[y] 15 copy y's satellite data into z 16 if color[y] = BLACK 17 then RB-DELETE-FIXUP(T, x) 18 return y
在刪除節點後,原紅黑樹的性質可能被改變,若是刪除的是紅色節點,那麼原紅黑樹的性質依舊保持,此時不用作修正操做,若是刪除的節點是黑色節點,原紅黑樹的性質可能會被改變,咱們要對其作修正操做。那麼哪些樹的性質會發生變化呢,若是刪除節點不是樹惟一節點,那麼刪除節點的那一個支的到各葉節點的黑色節點數會發生變化,此時性質5被破壞。若是被刪節點的惟物主惟一非空子節點是紅色,而被刪節點的父節點也是紅色,那麼性質4被破壞。若是被刪節點是根節點,而它的惟一非空子節點是紅色,則刪除後新根節點將變成紅色,違背性質2。
RB-DELETE-FIXUP(T, x) 恢復與保持紅黑性質的工做
snippet_file_name="blog_20140214_5_2156153" code_snippet_id="187675" snippet_file_name="blog_20140214_5_2156153" name="code"} 1 while x ≠ root[T] and color[x] = BLACK 2 do if x = left[p[x]] 3 then w ← right[p[x]] 4 if color[w] = RED 5 then color[w] ← BLACK ▹ Case 1 6 color[p[x]] ← RED ▹ Case 1 7 LEFT-ROTATE(T, p[x]) ▹ Case 1 8 w ← right[p[x]] ▹ Case 1 9 if color[left[w]] = BLACK and color[right[w]] = BLACK 10 then color[w] ← RED ▹ Case 2 11 x p[x] ▹ Case 2 12 else if color[right[w]] = BLACK 13 then color[left[w]] ← BLACK ▹ Case 3 14 color[w] ← RED ▹ Case 3 15 RIGHT-ROTATE(T, w) ▹ Case 3 16 w ← right[p[x]] ▹ Case 3 17 color[w] ← color[p[x]] ▹ Case 4 18 color[p[x]] ← BLACK ▹ Case 4 19 color[right[w]] ← BLACK ▹ Case 4 20 LEFT-ROTATE(T, p[x]) ▹ Case 4 21 x ← root[T] ▹ Case 4 22 else (same as then clause with "right" and "left" exchanged) 23 color[x] ← BLACK
上面的修復狀況看起來有些複雜,下面咱們用一個分析技巧:咱們從被刪節點後來頂替它的那個節點開始調整,並認爲它有額外的一重黑色。這裏額外一重黑色是什麼意思呢,咱們不是把紅黑樹的節點加上除紅與黑的另外一種顏色,這裏只是一種假設,咱們認爲咱們當前指向它,所以空有額外一種黑色,能夠認爲它的黑色是從它的父節點被刪除後繼承給它的,它如今能夠容納兩種顏色,若是它原來是紅色,那麼如今是紅+黑,若是原來是黑色,那麼它如今的顏色是黑+黑。有了這重額外的黑色,原紅黑樹性質5就能保持不變。如今只要花時是恢復其它性質就能夠了,作法仍是儘可能向根移動和窮舉全部可能性。
--saturnman。
紅黑樹刪除修復操做的幾種狀況@saturnman:
注:如下的狀況三、四、五、6,與上述算法導論之代碼RB-DELETE-FIXUP(T, x) 恢復與保持 中case1,case2,case3,case4相對應。
狀況1:當前節點是紅+黑色
解法,直接把當前節點染成黑色,結束。此時紅黑樹性質所有恢復。
狀況2:當前節點是黑+黑且是根節點
解法:什麼都不作,結束
狀況3:當前節點是黑+黑且兄弟節點爲紅色(此時父節點和兄弟節點的子節點分爲黑)。
解法:把父節點染成紅色,把兄弟節點染成黑色,以後從新進入算法(咱們只討論當前節點是其父節點左孩子時的狀況)。此變換後原紅黑樹性質5不變,而把問題轉化爲兄弟節點爲黑色的狀況(注:變化前,本來就未違反性質5,只是爲了把問題轉化爲兄弟節點爲黑色的狀況)。
變化前:
變化後:
狀況4:當前節點是黑加黑且兄弟是黑色且兄弟節點的兩個子節點全爲黑色。
解法:把當前節點和兄弟節點中抽取一重黑色追加到父節點上,把父節點當成新的當前節點,從新進入算法。(此變換後性質5不變)
變化前
變化後
狀況5:當前節點顏色是黑+黑,兄弟節點是黑色,兄弟的左子是紅色,右子是黑色。。
解法:把兄弟節點染紅,兄弟左子節點染黑,以後再在兄弟節點爲支點解右旋,以後從新進入算法。此是把當前的狀況轉化爲狀況6,而性質5得以保持。
變化前:
變化後:
狀況6:當前節點顏色是黑-黑色,它的兄弟節點是黑色,可是兄弟節點的右子是紅色,兄弟節點左子的顏色任意。
解法:把兄弟節點染成當前節點父節點的顏色,把當前節點父節點染成黑色,兄弟節點右子染成黑色,以後以當前節點的父節點爲支點進行左旋,此時算法結束,紅黑樹全部性質調整正確。
變化前:
變化後:
via July