目錄
1 紅黑樹的介紹
2 紅黑樹的應用
3 紅黑樹的時間複雜度和相關證實
4 紅黑樹的基本操做(一) 左旋和右旋
5 紅黑樹的基本操做(二) 添加
6 紅黑樹的基本操做(三) 刪除
html
做者:Sky Wang 於 2013-08-08 算法
概述:R-B Tree,又稱爲「紅黑樹」。本文參考了《算法導論》中紅黑樹相關知識,加之本身的理解,而後以圖文的形式對紅黑樹進行說明。本文的主要內容包括:紅黑樹的特性,紅黑樹的時間複雜度和它的證實,紅黑樹的左旋、右旋、插入、刪除等操做。數據結構
請尊重版權,轉載註明出處:http://www.cnblogs.com/skywang12345/p/3245399.html函數
更多內容: 數據結構與算法系列 目錄 post
(01) 紅黑樹(一)之 原理和算法詳細介紹
(02) 紅黑樹(二)之 C語言的實現
(03) 紅黑樹(三)之 Linux內核中紅黑樹的經典實現
(04) 紅黑樹(四)之 C++的實現
(05) 紅黑樹(五)之 Java的實現
(06) 紅黑樹(六)之 參考資料url
R-B Tree,全稱是Red-Black Tree,又稱爲「紅黑樹」,它一種特殊的二叉查找樹。紅黑樹的每一個節點上都有存儲位表示節點的顏色,能夠是紅(Red)或黑(Black)。spa
紅黑樹的特性:
(1)每一個節點或者是黑色,或者是紅色。
(2)根節點是黑色。
(3)每一個葉子節點(NIL)是黑色。 [注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!]
(4)若是一個節點是紅色的,則它的子節點必須是黑色的。
(5)從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點。.net
注意:
(01) 特性(3)中的葉子節點,是隻爲空(NIL或null)的節點。
(02) 特性(5),確保沒有一條路徑會比其餘路徑長出倆倍。於是,紅黑樹是相對是接近平衡的二叉樹。code
紅黑樹示意圖以下:htm
紅黑樹的應用比較普遍,主要是用它來存儲有序的數據,它的時間複雜度是O(lgn),效率很是之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虛擬內存的管理,都是經過紅黑樹去實現的。
紅黑樹的時間複雜度爲: O(lgn)
下面經過「數學概括法」對紅黑樹的時間複雜度進行證實。
定理:一棵含有n個節點的紅黑樹的高度至多爲2log(n+1).
證實:
"一棵含有n個節點的紅黑樹的高度至多爲2log(n+1)" 的逆否命題是 "高度爲h的紅黑樹,它的包含的內節點個數至少爲 2h/2-1個"。
咱們只須要證實逆否命題,便可證實原命題爲真;即只需證實 "高度爲h的紅黑樹,它的包含的內節點個數至少爲 2h/2-1個"。
從某個節點x出發(不包括該節點)到達一個葉節點的任意一條路徑上,黑色節點的個數稱爲該節點的黑高度(x's black height),記爲bh(x)。關於bh(x)有兩點須要說明:
第1點:根據紅黑樹的"特性(5) ,即從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點"可知,從節點x出發到達的全部的葉節點具備相同數目的黑節點。這也就意味着,bh(x)的值是惟一的!
第2點:根據紅黑色的"特性(4),即若是一個節點是紅色的,則它的子節點必須是黑色的"可知,從節點x出發達到葉節點"所經歷的黑節點數目">= "所經歷的紅節點的數目"。假設x是根節點,則能夠得出結論"bh(x) >= h/2"。進而,咱們只需證實 "高度爲h的紅黑樹,它的包含的黑節點個數至少爲 2bh(x)-1個"便可。
到這裏,咱們將須要證實的定理已經由
"一棵含有n個節點的紅黑樹的高度至多爲2log(n+1)"
轉變成只須要證實
"高度爲h的紅黑樹,它的包含的內節點個數至少爲 2bh(x)-1個"。
下面經過"數學概括法"開始論證高度爲h的紅黑樹,它的包含的內節點個數至少爲 2bh(x)-1個"。
(01) 當樹的高度h=0時,
內節點個數是0,bh(x) 爲0,2bh(x)-1 也爲 0。顯然,原命題成立。
(02) 當h>0,且樹的高度爲 h-1 時,它包含的節點個數至少爲 2bh(x)-1-1。這個是根據(01)推斷出來的!
下面,由樹的高度爲 h-1 的已知條件推出「樹的高度爲 h 時,它所包含的節點樹爲 2bh(x)-1」。
當樹的高度爲 h 時,
對於節點x(x爲根節點),其黑高度爲bh(x)。
對於節點x的左右子樹,它們黑高度爲 bh(x) 或者 bh(x)-1。
根據(02)的已知條件,咱們已知 "x的左右子樹,即高度爲 h-1 的節點,它包含的節點至少爲 2bh(x)-1-1 個";
因此,節點x所包含的節點至少爲 ( 2bh(x)-1-1 ) + ( 2bh(x)-1-1 ) + 1 = 2^bh(x)-1。即節點x所包含的節點至少爲 2bh(x)-1。
所以,原命題成立。
由(01)、(02)得出,"高度爲h的紅黑樹,它的包含的內節點個數至少爲 2^bh(x)-1個"。
所以,「一棵含有n個節點的紅黑樹的高度至多爲2log(n+1)」。
紅黑樹的基本操做是添加、刪除。在對紅黑樹進行添加或刪除以後,都會用到旋轉方法。爲何呢?道理很簡單,添加或刪除紅黑樹中的節點以後,紅黑樹就發生了變化,可能不知足紅黑樹的5條性質,也就再也不是一顆紅黑樹了,而是一顆普通的樹。而經過旋轉,可使這顆樹從新成爲紅黑樹。簡單點說,旋轉的目的是讓樹保持紅黑樹的特性。
旋轉包括兩種:左旋 和 右旋。下面分別對它們進行介紹。
對x進行左旋,意味着"將x變成一個左節點"。
左旋的僞代碼《算法導論》:參考上面的示意圖和下面的僞代碼,理解「紅黑樹T的節點x進行左旋」是如何進行的。
LEFT-ROTATE(T, x) 01 y ← right[x] // 前提:這裏假設x的右孩子爲y。下面開始正式操做 02 right[x] ← left[y] // 將 「y的左孩子」 設爲 「x的右孩子」,即 將β設爲x的右孩子 03 p[left[y]] ← x // 將 「x」 設爲 「y的左孩子的父親」,即 將β的父親設爲x 04 p[y] ← p[x] // 將 「x的父親」 設爲 「y的父親」 05 if p[x] = nil[T] 06 then root[T] ← y // 狀況1:若是 「x的父親」 是空節點,則將y設爲根節點 07 else if x = left[p[x]] 08 then left[p[x]] ← y // 狀況2:若是 x是它父節點的左孩子,則將y設爲「x的父節點的左孩子」 09 else right[p[x]] ← y // 狀況3:(x是它父節點的右孩子) 將y設爲「x的父節點的右孩子」 10 left[y] ← x // 將 「x」 設爲 「y的左孩子」 11 p[x] ← y // 將 「x的父節點」 設爲 「y」
理解左旋以後,看看下面一個更鮮明的例子。你能夠先不看右邊的結果,本身嘗試一下。
對x進行左旋,意味着"將x變成一個左節點"。
右旋的僞代碼《算法導論》:參考上面的示意圖和下面的僞代碼,理解「紅黑樹T的節點y進行右旋」是如何進行的。
RIGHT-ROTATE(T, y) 01 x ← left[y] // 前提:這裏假設y的左孩子爲x。下面開始正式操做 02 left[y] ← right[x] // 將 「x的右孩子」 設爲 「y的左孩子」,即 將β設爲y的左孩子 03 p[right[x]] ← y // 將 「y」 設爲 「x的右孩子的父親」,即 將β的父親設爲y 04 p[x] ← p[y] // 將 「y的父親」 設爲 「x的父親」 05 if p[y] = nil[T] 06 then root[T] ← x // 狀況1:若是 「y的父親」 是空節點,則將x設爲根節點 07 else if y = right[p[y]] 08 then right[p[y]] ← x // 狀況2:若是 y是它父節點的右孩子,則將x設爲「y的父節點的左孩子」 09 else left[p[y]] ← x // 狀況3:(y是它父節點的左孩子) 將x設爲「y的父節點的左孩子」 10 right[x] ← y // 將 「y」 設爲 「x的右孩子」 11 p[y] ← x // 將 「y的父節點」 設爲 「x」
理解右旋以後,看看下面一個更鮮明的例子。你能夠先不看右邊的結果,本身嘗試一下。
旋轉總結:
(01) 左旋 和 右旋 是相對的兩個概念,原理相似。理解一個也就理解了另外一個。
(02) 下面談談如何區分 左旋 和 右旋。
在實際應用中,若沒有完全理解 左旋 和 右旋,可能會將它們混淆。下面談談我對如何區分 左旋 和 右旋 的理解。
仔細觀察上面"左旋"和"右旋"的示意圖。咱們能清晰的發現,它們是對稱的。不管是左旋仍是右旋,被旋轉的樹,在旋轉前是二叉查找樹,而且旋轉以後仍然是一顆二叉查找樹。
左旋示例圖(以x爲節點進行左旋):
z x / / \ --(左旋)--> x y z / y
對x進行左旋,意味着,將「x的右孩子」設爲「x的父親節點」;即,將 x變成了一個左節點(x成了爲z的左孩子)!。 所以,左旋中的「左」,意味着「被旋轉的節點將變成一個左節點」。
右旋示例圖(以x爲節點進行右旋):
y x \ / \ --(右旋)--> x y z \ z
對x進行右旋,意味着,將「x的左孩子」設爲「x的父親節點」;即,將 x變成了一個右節點(x成了爲y的右孩子)! 所以,右旋中的「右」,意味着「被旋轉的節點將變成一個右節點」。
將一個節點插入到紅黑樹中,須要執行哪些步驟呢?首先,將紅黑樹看成一顆二叉查找樹,將節點插入;而後,將節點着色爲紅色;最後,經過旋轉和從新着色等方法來修正該樹,使之從新成爲一顆紅黑樹。詳細描述以下:
第一步: 將紅黑樹看成一顆二叉查找樹,將節點插入。
紅黑樹自己就是一顆二叉查找樹,將節點插入後,該樹仍然是一顆二叉查找樹。也就意味着,樹的鍵值仍然是有序的。此外,不管是左旋仍是右旋,若旋轉以前這棵樹是二叉查找樹,旋轉以後它必定仍是二叉查找樹。這也就意味着,任何的旋轉和從新着色操做,都不會改變它仍然是一顆二叉查找樹的事實。
好吧?那接下來,咱們就來千方百計的旋轉以及從新着色,使這顆樹從新成爲紅黑樹!
第二步:將插入的節點着色爲"紅色"。
爲何着色成紅色,而不是黑色呢?爲何呢?在回答以前,咱們須要從新溫習一下紅黑樹的特性:
(1) 每一個節點或者是黑色,或者是紅色。
(2) 根節點是黑色。
(3) 每一個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點!]
(4) 若是一個節點是紅色的,則它的子節點必須是黑色的。
(5) 從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點。
將插入的節點着色爲紅色,不會違背"特性(5)"!少違背一條特性,就意味着咱們須要處理的狀況越少。接下來,就要努力的讓這棵樹知足其它性質便可;知足了的話,它就又是一顆紅黑樹了。o(∩∩)o...哈哈
第三步: 經過一系列的旋轉或着色等操做,使之從新成爲一顆紅黑樹。
第二步中,將插入節點着色爲"紅色"以後,不會違背"特性(5)"。那它到底會違背哪些特性呢?
對於"特性(1)",顯然不會違背了。由於咱們已經將它塗成紅色了。
對於"特性(2)",顯然也不會違背。在第一步中,咱們是將紅黑樹看成二叉查找樹,而後執行的插入操做。而根據二叉查找數的特色,插入操做不會改變根節點。因此,根節點仍然是黑色。
對於"特性(3)",顯然不會違背了。這裏的葉子節點是指的空葉子節點,插入非空節點並不會對它們形成影響。
對於"特性(4)",是有可能違背的!
那接下來,想辦法使之"知足特性(4)",就能夠將樹從新構形成紅黑樹了。
下面看看代碼究竟是怎樣實現這三步的。
添加操做的僞代碼《算法導論》
RB-INSERT(T, z) 01 y ← nil[T] // 新建節點「y」,將y設爲空節點。 02 x ← root[T] // 設「紅黑樹T」的根節點爲「x」 03 while x ≠ nil[T] // 找出要插入的節點「z」在二叉樹T中的位置「y」 04 do y ← x 05 if key[z] < key[x] 06 then x ← left[x] 07 else x ← right[x] 08 p[z] ← y // 設置 「z的父親」 爲 「y」 09 if y = nil[T] 10 then root[T] ← z // 狀況1:若y是空節點,則將z設爲根 11 else if key[z] < key[y] 12 then left[y] ← z // 狀況2:若「z所包含的值」 < 「y所包含的值」,則將z設爲「y的左孩子」 13 else right[y] ← z // 狀況3:(「z所包含的值」 >= 「y所包含的值」)將z設爲「y的右孩子」 14 left[z] ← nil[T] // z的左孩子設爲空 15 right[z] ← nil[T] // z的右孩子設爲空。至此,已經完成將「節點z插入到二叉樹」中了。 16 color[z] ← RED // 將z着色爲「紅色」 17 RB-INSERT-FIXUP(T, z) // 經過RB-INSERT-FIXUP對紅黑樹的節點進行顏色修改以及旋轉,讓樹T仍然是一顆紅黑樹
結合僞代碼以及爲代碼上面的說明,先理解RB-INSERT。理解了RB-INSERT以後,咱們接着對 RB-INSERT-FIXUP的僞代碼進行說明。
添加修正操做的僞代碼《算法導論》
RB-INSERT-FIXUP(T, z) 01 while color[p[z]] = RED // 若「當前節點(z)的父節點是紅色」,則進行如下處理。 02 do if p[z] = left[p[p[z]]] // 若「z的父節點」是「z的祖父節點的左孩子」,則進行如下處理。 03 then y ← right[p[p[z]]] // 將y設置爲「z的叔叔節點(z的祖父節點的右孩子)」 04 if color[y] = RED // Case 1條件:叔叔是紅色 05 then color[p[z]] ← BLACK ▹ Case 1 // (01) 將「父節點」設爲黑色。 06 color[y] ← BLACK ▹ Case 1 // (02) 將「叔叔節點」設爲黑色。 07 color[p[p[z]]] ← RED ▹ Case 1 // (03) 將「祖父節點」設爲「紅色」。 08 z ← p[p[z]] ▹ Case 1 // (04) 將「祖父節點」設爲「當前節點」(紅色節點) 09 else if z = right[p[z]] // Case 2條件:叔叔是黑色,且當前節點是右孩子 10 then z ← p[z] ▹ Case 2 // (01) 將「父節點」做爲「新的當前節點」。 11 LEFT-ROTATE(T, z) ▹ Case 2 // (02) 以「新的當前節點」爲支點進行左旋。 12 color[p[z]] ← BLACK ▹ Case 3 // Case 3條件:叔叔是黑色,且當前節點是左孩子。(01) 將「父節點」設爲「黑色」。 13 color[p[p[z]]] ← RED ▹ Case 3 // (02) 將「祖父節點」設爲「紅色」。 14 RIGHT-ROTATE(T, p[p[z]]) ▹ Case 3 // (03) 以「祖父節點」爲支點進行右旋。 15 else (same as then clause with "right" and "left" exchanged) // 若「z的父節點」是「z的祖父節點的右孩子」,將上面的操做中「right」和「left」交換位置,而後依次執行。 16 color[root[T]] ← BLACK
根據被插入節點的父節點的狀況,能夠將"當節點z被着色爲紅色節點,並插入二叉樹"劃分爲三種狀況來處理。
① 狀況說明:被插入的節點是根節點。
處理方法:直接把此節點塗爲黑色。
② 狀況說明:被插入的節點的父節點是黑色。
處理方法:什麼也不須要作。節點被插入後,仍然是紅黑樹。
③ 狀況說明:被插入的節點的父節點是紅色。
處理方法:那麼,該狀況與紅黑樹的「特性(5)」相沖突。這種狀況下,被插入節點是必定存在非空祖父節點的;進一步的講,被插入節點也必定存在叔叔節點(即便叔叔節點爲空,咱們也視之爲存在,空節點自己就是黑色節點)。理解這點以後,咱們依據"叔叔節點的狀況",將這種狀況進一步劃分爲3種狀況(Case)。
現象說明 | 處理策略 | |
Case 1 | 當前節點的父節點是紅色,且當前節點的祖父節點的另外一個子節點(叔叔節點)也是紅色。 | (01) 將「父節點」設爲黑色。 |
Case 2 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子 | (01) 將「父節點」做爲「新的當前節點」。 |
Case 3 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子 | (01) 將「父節點」設爲「黑色」。 |
上面三種狀況(Case)處理問題的核心思路都是:將紅色的節點移到根節點;而後,將根節點設爲黑色。下面對它們詳細進行介紹。
1. (Case 1)叔叔是紅色
1.1 現象說明
當前節點(即,被插入節點)的父節點是紅色,且當前節點的祖父節點的另外一個子節點(叔叔節點)也是紅色。
1.2 處理策略
(01) 將「父節點」設爲黑色。
(02) 將「叔叔節點」設爲黑色。
(03) 將「祖父節點」設爲「紅色」。
(04) 將「祖父節點」設爲「當前節點」(紅色節點);即,以後繼續對「當前節點」進行操做。
下面談談爲何要這樣處理。(建議理解的時候,經過下面的圖進行對比)
「當前節點」和「父節點」都是紅色,違背「特性(4)」。因此,將「父節點」設置「黑色」以解決這個問題。
可是,將「父節點」由「紅色」變成「黑色」以後,違背了「特性(5)」:由於,包含「父節點」的分支的黑色節點的總數增長了1。 解決這個問題的辦法是:將「祖父節點」由「黑色」變成紅色,同時,將「叔叔節點」由「紅色」變成「黑色」。關於這裏,說明幾點:第一,爲何「祖父節點」以前是黑色?這個應該很容易想明白,由於在變換操做以前,該樹是紅黑樹,「父節點」是紅色,那麼「祖父節點」必定是黑色。 第二,爲何將「祖父節點」由「黑色」變成紅色,同時,將「叔叔節點」由「紅色」變成「黑色」;能解決「包含‘父節點’的分支的黑色節點的總數增長了1」的問題。這個道理也很簡單。「包含‘父節點’的分支的黑色節點的總數增長了1」 同時也意味着 「包含‘祖父節點’的分支的黑色節點的總數增長了1」,既然這樣,咱們經過將「祖父節點」由「黑色」變成「紅色」以解決「包含‘祖父節點’的分支的黑色節點的總數增長了1」的問題; 可是,這樣處理以後又會引發另外一個問題「包含‘叔叔’節點的分支的黑色節點的總數減小了1」,如今咱們已知「叔叔節點」是「紅色」,將「叔叔節點」設爲「黑色」就能解決這個問題。 因此,將「祖父節點」由「黑色」變成紅色,同時,將「叔叔節點」由「紅色」變成「黑色」;就解決了該問題。
按照上面的步驟處理以後:當前節點、父節點、叔叔節點之間都不會違背紅黑樹特性,但祖父節點卻不必定。若此時,祖父節點是根節點,直接將祖父節點設爲「黑色」,那就徹底解決這個問題了;若祖父節點不是根節點,那咱們須要將「祖父節點」設爲「新的當前節點」,接着對「新的當前節點」進行分析。
1.3 示意圖
2. (Case 2)叔叔是黑色,且當前節點是右孩子
2.1 現象說明
當前節點(即,被插入節點)的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子
2.2 處理策略
(01) 將「父節點」做爲「新的當前節點」。
(02) 以「新的當前節點」爲支點進行左旋。
下面談談爲何要這樣處理。(建議理解的時候,經過下面的圖進行對比)
首先,將「父節點」做爲「新的當前節點」;接着,以「新的當前節點」爲支點進行左旋。 爲了便於理解,咱們先說明第(02)步,再說明第(01)步;爲了便於說明,咱們設置「父節點」的代號爲F(Father),「當前節點」的代號爲S(Son)。
爲何要「以F爲支點進行左旋」呢?根據已知條件可知:S是F的右孩子。而以前咱們說過,咱們處理紅黑樹的核心思想:將紅色的節點移到根節點;而後,將根節點設爲黑色。既然是「將紅色的節點移到根節點」,那就是說要不斷的將破壞紅黑樹特性的紅色節點上移(即向根方向移動)。 而S又是一個右孩子,所以,咱們能夠經過「左旋」來將S上移!
按照上面的步驟(以F爲支點進行左旋)處理以後:若S變成了根節點,那麼直接將其設爲「黑色」,就徹底解決問題了;若S不是根節點,那咱們須要執行步驟(01),即「將F設爲‘新的當前節點’」。那爲何不繼續以S爲新的當前節點繼續處理,而須要以F爲新的當前節點來進行處理呢?這是由於「左旋」以後,F變成了S的「子節點」,即S變成了F的父節點;而咱們處理問題的時候,須要從下至上(由葉到根)方向進行處理;也就是說,必須先解決「孩子」的問題,再解決「父親」的問題;因此,咱們執行步驟(01):將「父節點」做爲「新的當前節點」。
2.2 示意圖
3. (Case 3)叔叔是黑色,且當前節點是左孩子
3.1 現象說明
當前節點(即,被插入節點)的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子
3.2 處理策略
(01) 將「父節點」設爲「黑色」。
(02) 將「祖父節點」設爲「紅色」。
(03) 以「祖父節點」爲支點進行右旋。
下面談談爲何要這樣處理。(建議理解的時候,經過下面的圖進行對比)
爲了便於說明,咱們設置「當前節點」爲S(Original Son),「兄弟節點」爲B(Brother),「叔叔節點」爲U(Uncle),「父節點」爲F(Father),祖父節點爲G(Grand-Father)。
S和F都是紅色,違背了紅黑樹的「特性(4)」,咱們能夠將F由「紅色」變爲「黑色」,就解決了「違背‘特性(4)’」的問題;但卻引發了其它問題:違背特性(5),由於將F由紅色改成黑色以後,全部通過F的分支的黑色節點的個數增長了1。那咱們如何解決「全部通過F的分支的黑色節點的個數增長了1」的問題呢? 咱們能夠經過「將G由黑色變成紅色」,同時「以G爲支點進行右旋」來解決。
2.3 示意圖
提示:上面的進行Case 3處理以後,再將節點"120"看成當前節點,就變成了Case 2的狀況。
將紅黑樹內的某一個節點刪除。須要執行的操做依次是:首先,將紅黑樹看成一顆二叉查找樹,將該節點從二叉查找樹中刪除;而後,經過"旋轉和從新着色"等一系列來修正該樹,使之從新成爲一棵紅黑樹。詳細描述以下:
第一步:將紅黑樹看成一顆二叉查找樹,將節點刪除。
這和"刪除常規二叉查找樹中刪除節點的方法是同樣的"。分3種狀況:
① 被刪除節點沒有兒子,即爲葉節點。那麼,直接將該節點刪除就OK了。
② 被刪除節點只有一個兒子。那麼,直接刪除該節點,並用該節點的惟一子節點頂替它的位置。
③ 被刪除節點有兩個兒子。那麼,先找出它的後繼節點;而後把「它的後繼節點的內容」複製給「該節點的內容」;以後,刪除「它的後繼節點」。在這裏,後繼節點至關於替身,在將後繼節點的內容複製給"被刪除節點"以後,再將後繼節點刪除。這樣就巧妙的將問題轉換爲"刪除後繼節點"的狀況了,下面就考慮後繼節點。 在"被刪除節點"有兩個非空子節點的狀況下,它的後繼節點不多是雙子非空。既然"的後繼節點"不可能雙子都非空,就意味着"該節點的後繼節點"要麼沒有兒子,要麼只有一個兒子。若沒有兒子,則按"狀況① "進行處理;若只有一個兒子,則按"狀況② "進行處理。
第二步:經過"旋轉和從新着色"等一系列來修正該樹,使之從新成爲一棵紅黑樹。
由於"第一步"中刪除節點以後,可能會違背紅黑樹的特性。因此須要經過"旋轉和從新着色"來修正該樹,使之從新成爲一棵紅黑樹。
刪除操做的僞代碼《算法導論》
RB-DELETE(T, z) 01 if left[z] = nil[T] or right[z] = nil[T] 02 then y ← z // 若「z的左孩子」 或 「z的右孩子」爲空,則將「z」賦值給 「y」; 03 else y ← TREE-SUCCESSOR(z) // 不然,將「z的後繼節點」賦值給 「y」。 04 if left[y] ≠ nil[T] 05 then x ← left[y] // 若「y的左孩子」 不爲空,則將「y的左孩子」 賦值給 「x」; 06 else x ← right[y] // 不然,「y的右孩子」 賦值給 「x」。 07 p[x] ← p[y] // 將「y的父節點」 設置爲 「x的父節點」 08 if p[y] = nil[T] 09 then root[T] ← x // 狀況1:若「y的父節點」 爲空,則設置「x」 爲 「根節點」。 10 else if y = left[p[y]] 11 then left[p[y]] ← x // 狀況2:若「y是它父節點的左孩子」,則設置「x」 爲 「y的父節點的左孩子」 12 else right[p[y]] ← x // 狀況3:若「y是它父節點的右孩子」,則設置「x」 爲 「y的父節點的右孩子」 13 if y ≠ z 14 then key[z] ← key[y] // 若「y的值」 賦值給 「z」。注意:這裏只拷貝z的值給y,而沒有拷貝z的顏色!!! 15 copy y's satellite data into z 16 if color[y] = BLACK 17 then RB-DELETE-FIXUP(T, x) // 若「y爲黑節點」,則調用 18 return y
結合僞代碼以及爲代碼上面的說明,先理解RB-DELETE。理解了RB-DELETE以後,接着對 RB-DELETE-FIXUP的僞代碼進行說明
RB-DELETE-FIXUP(T, x) 01 while x ≠ root[T] and color[x] = BLACK 02 do if x = left[p[x]] 03 then w ← right[p[x]] // 若 「x」是「它父節點的左孩子」,則設置 「w」爲「x的叔叔」(即x爲它父節點的右孩子) 04 if color[w] = RED // Case 1: x是「黑+黑」節點,x的兄弟節點是紅色。(此時x的父節點和x的兄弟節點的子節點都是黑節點)。 05 then color[w] ← BLACK ▹ Case 1 // (01) 將x的兄弟節點設爲「黑色」。 06 color[p[x]] ← RED ▹ Case 1 // (02) 將x的父節點設爲「紅色」。 07 LEFT-ROTATE(T, p[x]) ▹ Case 1 // (03) 對x的父節點進行左旋。 08 w ← right[p[x]] ▹ Case 1 // (04) 左旋後,從新設置x的兄弟節點。 09 if color[left[w]] = BLACK and color[right[w]] = BLACK // Case 2: x是「黑+黑」節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。 10 then color[w] ← RED ▹ Case 2 // (01) 將x的兄弟節點設爲「紅色」。 11 x ← p[x] ▹ Case 2 // (02) 設置「x的父節點」爲「新的x節點」。 12 else if color[right[w]] = BLACK // Case 3: x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。 13 then color[left[w]] ← BLACK ▹ Case 3 // (01) 將x兄弟節點的左孩子設爲「黑色」。 14 color[w] ← RED ▹ Case 3 // (02) 將x兄弟節點設爲「紅色」。 15 RIGHT-ROTATE(T, w) ▹ Case 3 // (03) 對x的兄弟節點進行右旋。 16 w ← right[p[x]] ▹ Case 3 // (04) 右旋後,從新設置x的兄弟節點。 17 color[w] ← color[p[x]] ▹ Case 4 // Case 4: x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的。(01) 將x父節點顏色 賦值給 x的兄弟節點。 18 color[p[x]] ← BLACK ▹ Case 4 // (02) 將x父節點設爲「黑色」。 19 color[right[w]] ← BLACK ▹ Case 4 // (03) 將x兄弟節點的右子節設爲「黑色」。 20 LEFT-ROTATE(T, p[x]) ▹ Case 4 // (04) 對x的父節點進行左旋。 21 x ← root[T] ▹ Case 4 // (05) 設置「x」爲「根節點」。 22 else (same as then clause with "right" and "left" exchanged) // 若 「x」是「它父節點的右孩子」,將上面的操做中「right」和「left」交換位置,而後依次執行。 23 color[x] ← BLACK
下面對刪除函數進行分析。在分析以前,咱們再次溫習一下紅黑樹的幾個特性:
(1) 每一個節點或者是黑色,或者是紅色。
(2) 根節點是黑色。
(3) 每一個葉子節點是黑色。 [注意:這裏葉子節點,是指爲空的葉子節點!]
(4) 若是一個節點是紅色的,則它的子節點必須是黑色的。
(5) 從一個節點到該節點的子孫節點的全部路徑上包含相同數目的黑節點。
前面咱們將"刪除紅黑樹中的節點"大體分爲兩步,在第一步中"將紅黑樹看成一顆二叉查找樹,將節點刪除"後,可能違反"特性(2)、(4)、(5)"三個特性。第二步須要解決上面的三個問題,進而保持紅黑樹的所有特性。
爲了便於分析,咱們假設"x包含一個額外的黑色"(x本來的顏色還存在),這樣就不會違反"特性(5)"。爲何呢?
經過RB-DELETE算法,咱們知道:刪除節點y以後,x佔據了原來節點y的位置。 既然刪除y(y是黑色),意味着減小一個黑色節點;那麼,再在該位置上增長一個黑色便可。這樣,當咱們假設"x包含一個額外的黑色",就正好彌補了"刪除y所丟失的黑色節點",也就不會違反"特性(5)"。 所以,假設"x包含一個額外的黑色"(x本來的顏色還存在),這樣就不會違反"特性(5)"。
如今,x不只包含它本來的顏色屬性,x還包含一個額外的黑色。即x的顏色屬性是"紅+黑"或"黑+黑",它違反了"特性(1)"。
如今,咱們面臨的問題,由解決"違反了特性(2)、(4)、(5)三個特性"轉換成了"解決違反特性(1)、(2)、(4)三個特性"。RB-DELETE-FIXUP須要作的就是經過算法恢復紅黑樹的特性(1)、(2)、(4)。RB-DELETE-FIXUP的思想是:將x所包含的額外的黑色不斷沿樹上移(向根方向移動),直到出現下面的姿態:
a) x指向一個"紅+黑"節點。此時,將x設爲一個"黑"節點便可。
b) x指向根。此時,將x設爲一個"黑"節點便可。
c) 非前面兩種姿態。
將上面的姿態,能夠歸納爲3種狀況。
① 狀況說明:x是「紅+黑」節點。
處理方法:直接把x設爲黑色,結束。此時紅黑樹性質所有恢復。
② 狀況說明:x是「黑+黑」節點,且x是根。
處理方法:什麼都不作,結束。此時紅黑樹性質所有恢復。
③ 狀況說明:x是「黑+黑」節點,且x不是根。
處理方法:這種狀況又能夠劃分爲4種子狀況。這4種子狀況以下表所示:
現象說明 | 處理策略 | |
Case 1 | x是"黑+黑"節點,x的兄弟節點是紅色。(此時x的父節點和x的兄弟節點的子節點都是黑節點)。 | (01) 將x的兄弟節點設爲「黑色」。 |
Case 2 | x是「黑+黑」節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。 | (01) 將x的兄弟節點設爲「紅色」。 |
Case 3 | x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。 | (01) 將x兄弟節點的左孩子設爲「黑色」。 |
Case 4 | x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的,x的兄弟節點的左孩子任意顏色。 | (01) 將x父節點顏色 賦值給 x的兄弟節點。 |
1. (Case 1)x是"黑+黑"節點,x的兄弟節點是紅色
1.1 現象說明
x是"黑+黑"節點,x的兄弟節點是紅色。(此時x的父節點和x的兄弟節點的子節點都是黑節點)。
1.2 處理策略
(01) 將x的兄弟節點設爲「黑色」。
(02) 將x的父節點設爲「紅色」。
(03) 對x的父節點進行左旋。
(04) 左旋後,從新設置x的兄弟節點。
下面談談爲何要這樣處理。(建議理解的時候,經過下面的圖進行對比)
這樣作的目的是將「Case 1」轉換爲「Case 2」、「Case 3」或「Case 4」,從而進行進一步的處理。對x的父節點進行左旋;左旋後,爲了保持紅黑樹特性,就須要在左旋前「將x的兄弟節點設爲黑色」,同時「將x的父節點設爲紅色」;左旋後,因爲x的兄弟節點發生了變化,須要更新x的兄弟節點,從而進行後續處理。
1.3 示意圖
2. (Case 2) x是"黑+黑"節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色
2.1 現象說明
x是「黑+黑」節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。
2.2 處理策略
(01) 將x的兄弟節點設爲「紅色」。
(02) 設置「x的父節點」爲「新的x節點」。
下面談談爲何要這樣處理。(建議理解的時候,經過下面的圖進行對比)
這個狀況的處理思想:是將「x中多餘的一個黑色屬性上移(往根方向移動)」。 x是「黑+黑」節點,咱們將x由「黑+黑」節點 變成 「黑」節點,多餘的一個「黑」屬性移到x的父節點中,即x的父節點多出了一個黑屬性(若x的父節點原先是「黑」,則此時變成了「黑+黑」;若x的父節點原先時「紅」,則此時變成了「紅+黑」)。 此時,須要注意的是:全部通過x的分支中黑節點個數沒變化;可是,全部通過x的兄弟節點的分支中黑色節點的個數增長了1(由於x的父節點多了一個黑色屬性)!爲了解決這個問題,咱們須要將「全部通過x的兄弟節點的分支中黑色節點的個數減1」便可,那麼就能夠經過「將x的兄弟節點由黑色變成紅色」來實現。
通過上面的步驟(將x的兄弟節點設爲紅色),多餘的一個顏色屬性(黑色)已經跑到x的父節點中。咱們須要將x的父節點設爲「新的x節點」進行處理。若「新的x節點」是「黑+紅」,直接將「新的x節點」設爲黑色,便可徹底解決該問題;若「新的x節點」是「黑+黑」,則須要對「新的x節點」進行進一步處理。
2.3 示意圖
3. (Case 3)x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的
3.1 現象說明
x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。
3.2 處理策略
(01) 將x兄弟節點的左孩子設爲「黑色」。
(02) 將x兄弟節點設爲「紅色」。
(03) 對x的兄弟節點進行右旋。
(04) 右旋後,從新設置x的兄弟節點。
下面談談爲何要這樣處理。(建議理解的時候,經過下面的圖進行對比)
咱們處理「Case 3」的目的是爲了將「Case 3」進行轉換,轉換成「Case 4」,從而進行進一步的處理。轉換的方式是對x的兄弟節點進行右旋;爲了保證右旋後,它仍然是紅黑樹,就須要在右旋前「將x的兄弟節點的左孩子設爲黑色」,同時「將x的兄弟節點設爲紅色」;右旋後,因爲x的兄弟節點發生了變化,須要更新x的兄弟節點,從而進行後續處理。
3.3 示意圖
4. (Case 4)x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的,x的兄弟節點的左孩子任意顏色
4.1 現象說明
x是「黑+黑」節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的,x的兄弟節點的左孩子任意顏色。
4.2 處理策略
(01) 將x父節點顏色 賦值給 x的兄弟節點。
(02) 將x父節點設爲「黑色」。
(03) 將x兄弟節點的右子節設爲「黑色」。
(04) 對x的父節點進行左旋。
(05) 設置「x」爲「根節點」。
下面談談爲何要這樣處理。(建議理解的時候,經過下面的圖進行對比)
咱們處理「Case 4」的目的是:去掉x中額外的黑色,將x變成單獨的黑色。處理的方式是「:進行顏色修改,而後對x的父節點進行左旋。下面,咱們來分析是如何實現的。
爲了便於說明,咱們設置「當前節點」爲S(Original Son),「兄弟節點」爲B(Brother),「兄弟節點的左孩子」爲BLS(Brother's Left Son),「兄弟節點的右孩子」爲BRS(Brother's Right Son),「父節點」爲F(Father)。
咱們要對F進行左旋。但在左旋前,咱們須要調換F和B的顏色,並設置BRS爲黑色。爲何須要這裏處理呢?由於左旋後,F和BLS是父子關係,而咱們已知BL是紅色,若是F是紅色,則違背了「特性(4)」;爲了解決這一問題,咱們將「F設置爲黑色」。 可是,F設置爲黑色以後,爲了保證知足「特性(5)」,即爲了保證左旋以後:
第一,「同時通過根節點和S的分支的黑色節點個數不變」。
若知足「第一」,只須要S丟棄它多餘的顏色便可。由於S的顏色是「黑+黑」,而左旋後「同時通過根節點和S的分支的黑色節點個數」增長了1;如今,只需將S由「黑+黑」變成單獨的「黑」節點,便可知足「第一」。
第二,「同時通過根節點和BLS的分支的黑色節點數不變」。
若知足「第二」,只須要將「F的原始顏色」賦值給B便可。以前,咱們已經將「F設置爲黑色」(即,將B的顏色"黑色",賦值給了F)。至此,咱們算是調換了F和B的顏色。
第三,「同時通過根節點和BRS的分支的黑色節點數不變」。
在「第二」已經知足的狀況下,若要知足「第三」,只須要將BRS設置爲「黑色」便可。
通過,上面的處理以後。紅黑樹的特性所有獲得的知足!接着,咱們將x設爲根節點,就能夠跳出while循環(參考僞代碼);即完成了所有處理。
至此,咱們就完成了Case 4的處理。理解Case 4的核心,是瞭解如何「去掉當前節點額外的黑色」。
4.3 示意圖
OK!至此,紅黑樹的理論知識差很少講完了。後續再更新紅黑樹的實現代碼!
1, 《算法導論》
2, 教你透徹瞭解紅黑樹