# 教你透徹了解紅黑樹
## 二叉查找樹
由于紅黑樹本質上就是一棵二叉查找樹,所以在了解紅黑樹之前,咱們先來看下二叉查找樹。
二叉查找樹(Binary Search Tree),也稱有序二叉樹(ordered binary tree),排序二叉樹(sorted binary tree),是指一棵空樹或者具有下列性質的二叉樹:
- 若任意結點的左子樹不空,則左子樹上所有結點的值均小于它的根結點的值;
- 若任意結點的右子樹不空,則右子樹上所有結點的值均大于它的根結點的值;
- 任意結點的左、右子樹也分別為二叉查找樹。
- 沒有鍵值相等的結點(no duplicate nodes)。
因為,一棵由n個結點,隨機構造的二叉查找樹的高度為lgn,所以順理成章,一般操作的執行時間為O(lgn).(至于n個結點的二叉樹高度為lgn的證明,可參考算法導論 第12章 二叉查找樹 第12.4節)。
但二叉樹若退化成了一棵具有n個結點的線性鏈后,則此些操作最壞情況運行時間為O(n)。后面我們會看到一種基于二叉查找樹-紅黑樹,它通過一些性質使得樹相對平衡,使得最終查找、插入、刪除的時間復雜度最壞情況下依然為O(lgn)。
## 紅黑樹
前面我們已經說過,紅黑樹,本質上來說就是一棵二叉查找樹,但它在二叉查找樹的基礎上增加了著色和相關的性質使得紅黑樹相對平衡,從而保證了紅黑樹的查找、插入、刪除的時間復雜度最壞為O(log n)。
但它是如何保證一棵n個結點的紅黑樹的高度始終保持在h = logn的呢?這就引出了紅黑樹的5條性質:
1)每個結點要么是紅的,要么是黑的。
2)根結點是黑的。
3)每個葉結點(葉結點即指樹尾端NIL指針或NULL結點)是黑的。
4)如果一個結點是紅的,那么它的倆個兒子都是黑的。
5)對于任一結點而言,其到葉結點樹尾端NIL指針的每一條路徑都包含相同數目的黑結點。
正是紅黑樹的這5條性質,使得一棵n個結點是紅黑樹始終保持了logn的高度,從而也就解釋了上面我們所說的“紅黑樹的查找、插入、刪除的時間復雜度最壞為O(log n)”這一結論的原因。
如下圖所示,即是一顆紅黑樹(下圖引自wikipedia:<http://t.cn/hgvH1l>):

上文中我們所說的 "葉結點" 或"NULL結點",它不包含數據而只充當樹在此結束的指示,這些結點以及它們的父結點,在繪圖中都會經常被省略。
### 樹的旋轉知識
當我們在對紅黑樹進行插入和刪除等操作時,對樹做了修改,那么可能會違背紅黑樹的性質。
為了繼續保持紅黑樹的性質,我們可以通過對結點進行重新著色,以及對樹進行相關的旋轉操作,即修改樹中某些結點的顏色及指針結構,來達到對紅黑樹進行插入或刪除結點等操作后,繼續保持它的性質或平衡。
樹的旋轉,分為左旋和右旋,以下借助圖來做形象的解釋和介紹:
1.左旋

如上圖所示:
當在某個結點pivot上,做左旋操作時,我們假設它的右孩子y不是NIL[T],pivot可以為任何不是NIL[T]的左孩子結點。
左旋以pivot到y之間的鏈為“支軸”進行,它使y成為該孩子樹新的根,而y的左孩子b則成為pivot的右孩子。
左旋操作的參考代碼如下所示(以x代替上述的pivot):
```
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
```
2.右旋
右旋與左旋差不多,再此不做詳細介紹。

對于樹的旋轉,能保持不變的只有原樹的搜索性質,而原樹的紅黑性質則不能保持,在紅黑樹的數據插入和刪除后可利用旋轉和顏色重涂來恢復樹的紅黑性質。
### 紅黑樹的插入
要真正理解紅黑樹的插入和刪除,還得先理解二叉查找樹的插入和刪除。磨刀不誤砍柴工,咱們再來分別了解下二叉查找樹的插入和刪除。
#### 二叉查找樹的插入
如果要在二叉查找樹中插入一個結點,首先要查找到結點插入的位置,然后進行插入,假設插入的結點為z的話,插入的偽代碼如下:
```
TREE-INSERT(T, z)
1 y ← NIL
2 x ← root[T]
3 while x ≠ NIL
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
10 then root[T] ← z ? Tree T was empty
11 else if key[z] < key[y]
12 then left[y] ← z
13 else right[y] ← z
```
可以看到,上述第3-7行代碼即是在二叉查找樹中查找z待插入的位置,如果插入結點z小于當前遍歷到的結點,則到當前結點的左子樹中繼續查找,如果z大于當前結點,則到當前結點的右子樹中繼續查找,第9-13行代碼找到待插入的位置,如果z依然比此刻遍歷到的新的當前結點小,則z作為當前結點的左孩子,否則作為當前結點的右孩子。
#### 紅黑樹的插入和插入修復
現在我們了解了二叉查找樹的插入,接下來,咱們便來具體了解紅黑樹的插入操作。紅黑樹的插入相當于在二叉查找樹插入的基礎上,為了重新恢復平衡,繼續做了插入修復操作。
假設插入的結點為z,紅黑樹的插入偽代碼具體如下所示:
```
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)前面的第1-13行代碼基本就是二叉查找樹的插入代碼,然后第14-16行代碼把z的左孩子、右孩子都賦為葉結點nil,再把z結點著為紅色,最后為保證紅黑性質在插入操作后依然保持,調用一個輔助程序RB-INSERT-FIXUP來對結點進行重新著色,并旋轉。
換言之
- 如果插入的是根結點,因為原樹是空樹,此情況只會違反性質2,所以直接把此結點涂為黑色。
- 如果插入的結點的父結點是黑色,由于此不會違反性質2和性質4,紅黑樹沒有被破壞,所以此時也是什么也不做。
但當遇到下述3種情況時:
- 插入修復情況1:如果當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色
- 插入修復情況2:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的右子
- 插入修復情況3:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的左子
又該如何調整呢?答案就是根據紅黑樹插入代碼RB-INSERT(T, z)最后一行調用的RB-INSERT-FIXUP(T,z)所示操作進行,具體如下所示:
```
RB-INSERT-FIXUP(T,z)
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
```
下面,咱們來分別處理上述3種插入修復情況。
**插入修復情況1:當前結點的父結點是紅色且祖父結點的另一個子結點(叔叔結點)是紅色。**
即如下代碼所示:
```
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
```
針對情況1,變化前(圖片來源:saturnman)[當前結點為4結點]:

變化后:

**插入修復情況2:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的右子**
對策:當前結點的父結點做為新的當前結點,以新當前結點為支點左旋。即如下代碼所示:
```
9 else if z = right[p[z]]
10 then z ← p[z] ? Case 2
11 LEFT-ROTATE(T, z) ? Case 2
```
如下圖所示,變化前[當前結點為7結點]:

變化后:

**插入修復情況3:當前結點的父結點是紅色,叔叔結點是黑色,當前結點是其父結點的左子**
解法:父結點變為黑色,祖父結點變為紅色,在祖父結點為支點右旋,操作代碼為:
```
12 color[p[z]] ← BLACK ? Case 3
13 color[p[p[z]]] ← RED ? Case 3
14 RIGHT-ROTATE(T, p[p[z]]) ? Case 3
```
最后,把根結點涂為黑色,整棵紅黑樹便重新恢復了平衡。
如下圖所示[當前結點為2結點]

變化后:

### 紅黑樹的刪除
ok,接下來,咱們最后來了解,紅黑樹的刪除操作。
"我們刪除的結點的方法與常規二叉搜索樹中刪除結點的方法是一樣的,如果被刪除的結點不是有雙非空子女,則直接刪除這個結點,用它的唯一子結點頂替它的位置,如果它的子結點分是空結點,那就用空結點頂替它的位置,如果它的雙子全為非空,我們就把它的直接后繼結點內容復制到它的位置,之后以同樣的方式刪除它的后繼結點,它的后繼結點不可能是雙子非空,因此此傳遞過程最多只進行一次。”
#### 二叉查找樹的刪除
繼續講解之前,補充說明下二叉樹結點刪除的幾種情況,待刪除的結點按照兒子的個數可以分為三種:
1. 沒有兒子,即為葉結點。直接把父結點的對應兒子指針設為NULL,刪除兒子結點就OK了。
2. 只有一個兒子。那么把父結點的相應兒子指針指向兒子的獨生子,刪除兒子結點也OK了。
3. 有兩個兒子。這是最麻煩的情況,因為你刪除結點之后,還要保證滿足搜索二叉樹的結構。其實也比較容易,我們可以選擇左兒子中的最大元素或者右兒子中的最小元素放到待刪除結點的位置,就可以保證結構的不變。當然,你要記得調整子樹,畢竟又出現了結點刪除。習慣上大家選擇左兒子中的最大元素,其實選擇右兒子的最小元素也一樣,沒有任何差別,只是人們習慣從左向右。這里咱們也選擇左兒子的最大元素,將它放到待刪結點的位置。左兒子的最大元素其實很好找,只要順著左兒子不斷的去搜索右子樹就可以了,直到找到一個沒有右子樹的結點。那就是最大的了。
二叉查找樹的刪除代碼如下所示:
```
TREE-DELETE(T, z)
1 if left[z] = NIL or right[z] = NIL
2 then y ← z
3 else y ← TREE-SUCCESSOR(z)
4 if left[y] ≠ NIL
5 then x ← left[y]
6 else x ← right[y]
7 if x ≠ NIL
8 then p[x] ← p[y]
9 if p[y] = NIL
10 then root[T] ← x
11 else if y = left[p[y]]
12 then left[p[y]] ← x
13 else right[p[y]] ← x
14 if y ≠ z
15 then key[z] ← key[y]
16 copy y's satellite data into z
17 return y
```
#### 紅黑樹的刪除和刪除修復
OK,回到紅黑樹上來,紅黑樹結點刪除的算法實現是:
RB-DELETE(T, z) 單純刪除結點的總操作
```
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 ≠ 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) 恢復與保持紅黑性質的工作
```
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。
如果是以下情況,恢復比較簡單:
- a)當前結點是紅+黑色
解法,直接把當前結點染成黑色,結束此時紅黑樹性質全部恢復。
- b)當前結點是黑+黑且是根結點,
解法:什么都不做,結束。
但如果是以下情況呢?:
- 刪除修復情況1:當前結點是黑+黑且兄弟結點為紅色(此時父結點和兄弟結點的子結點分為黑)
- 刪除修復情況2:當前結點是黑加黑且兄弟是黑色且兄弟結點的兩個子結點全為黑色
- 刪除修復情況3:當前結點顏色是黑+黑,兄弟結點是黑色,兄弟的左子是紅色,右子是黑色
- 刪除修復情況4:當前結點顏色是黑-黑色,它的兄弟結點是黑色,但是兄弟結點的右子是紅色,兄弟結點左子的顏色任意
此時,我們需要調用RB-DELETE-FIXUP(T, x),來恢復與保持紅黑性質的工作。
下面,咱們便來分別處理這4種刪除修復情況。
**刪除修復情況1:當前結點是黑+黑且兄弟結點為紅色(此時父結點和兄弟結點的子結點分為黑)。**
解法:把父結點染成紅色,把兄弟結點染成黑色,之后重新進入算法(我們只討論當前結點是其父結點左孩子時的情況)。此變換后原紅黑樹性質5不變,而把問題轉化為兄弟結點為黑色的情況(注:變化前,原本就未違反性質5,只是為了**把問題轉化為兄弟結點為黑色的情況**)。 即如下代碼操作:
```
//調用RB-DELETE-FIXUP(T, x) 的1-8行代碼
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
```
變化前:

變化后:

**刪除修復情況2:當前結點是黑加黑且兄弟是黑色且兄弟結點的兩個子結點全為黑色。**
解法:把當前結點和兄弟結點中抽取一重黑色追加到父結點上,把父結點當成新的當前結點,重新進入算法。(此變換后性質5不變),即調用RB-INSERT-FIXUP(T, z) 的第9-10行代碼操作,如下:
```
//調用RB-DELETE-FIXUP(T, x) 的9-11行代碼
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED ? Case 2
11 x p[x] ? Case 2
```
變化前:

變化后:

**刪除修復情況3:當前結點顏色是黑+黑,兄弟結點是黑色,兄弟的左子是紅色,右子是黑色。**
解法:把兄弟結點染紅,兄弟左子結點染黑,之后再在兄弟結點為支點解右旋,之后重新進入算法。此是把當前的情況轉化為情況4,而性質5得以保持,即調用RB-INSERT-FIXUP(T, z) 的第12-16行代碼,如下所示:
```
//調用RB-DELETE-FIXUP(T, x) 的第12-16行代碼
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
```
變化前:

變化后:

**刪除修復情況4:當前結點顏色是黑-黑色,它的兄弟結點是黑色,但是兄弟結點的右子是紅色,兄弟結點左子的顏色任意。**
解法:把兄弟結點染成當前結點父結點的顏色,把當前結點父結點染成黑色,兄弟結點右子染成黑色,之后以當前結點的父結點為支點進行左旋,此時算法結束,紅黑樹所有性質調整正確,即調用RB-INSERT-FIXUP(T, z)的第17-21行代碼,如下所示:
```
//調用RB-DELETE-FIXUP(T, x) 的第17-21行代碼
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
```
變化前:

變化后:

### 本文參考
本文參考了算法導論、STL源碼剖析、計算機程序設計藝術等資料,并推薦閱讀這個PDF:Left-Leaning Red-Black Trees, Dagstuhl Workshop on Data Structures, Wadern, Germany, February, 2008.
下載地址:<http://www.cs.princeton.edu/~rs/talks/LLRB/RedBlack.pdf>。
- 程序員如何準備面試中的算法
- 第一部分 數據結構
- 第一章 字符串
- 1.0 本章導讀
- 1.1 旋轉字符串
- 1.2 字符串包含
- 1.3 字符串轉換成整數
- 1.4 回文判斷
- 1.5 最長回文子串
- 1.6 字符串的全排列
- 1.10 本章習題
- 第二章 數組
- 2.0 本章導讀
- 2.1 尋找最小的 k 個數
- 2.2 尋找和為定值的兩個數
- 2.3 尋找和為定值的多個數
- 2.4 最大連續子數組和
- 2.5 跳臺階
- 2.6 奇偶排序
- 2.7 荷蘭國旗
- 2.8 矩陣相乘
- 2.9 完美洗牌
- 2.15 本章習題
- 第三章 樹
- 3.0 本章導讀
- 3.1 紅黑樹
- 3.2 B樹
- 3.3 最近公共祖先LCA
- 3.10 本章習題
- 第二部分 算法心得
- 第四章 查找匹配
- 4.1 有序數組的查找
- 4.2 行列遞增矩陣的查找
- 4.3 出現次數超過一半的數字
- 第五章 動態規劃
- 5.0 本章導讀
- 5.1 最大連續乘積子串
- 5.2 字符串編輯距離
- 5.3 格子取數
- 5.4 交替字符串
- 5.10 本章習題
- 第三部分 綜合演練
- 第六章 海量數據處理
- 6.0 本章導讀
- 6.1 關聯式容器
- 6.2 分而治之
- 6.3 simhash算法
- 6.4 外排序
- 6.5 MapReduce
- 6.6 多層劃分
- 6.7 Bitmap
- 6.8 Bloom filter
- 6.9 Trie樹
- 6.10 數據庫
- 6.11 倒排索引
- 6.15 本章習題
- 第七章 機器學習
- 7.1 K 近鄰算法
- 7.2 支持向量機
- 附錄 更多題型
- 附錄A 語言基礎
- 附錄B 概率統計
- 附錄C 智力邏輯
- 附錄D 系統設計
- 附錄E 操作系統
- 附錄F 網絡協議
- sift算法
- sift算法的編譯與實現
- 教你一步一步用c語言實現sift算法、上
- 教你一步一步用c語言實現sift算法、下
- 其它
- 40億個數中快速查找
- hash表算法
- 一致性哈希算法
- 倒排索引關鍵詞不重復Hash編碼
- 傅里葉變換算法、上
- 傅里葉變換算法、下
- 后綴樹
- 基于給定的文檔生成倒排索引的編碼與實踐
- 搜索關鍵詞智能提示suggestion
- 最小操作數
- 最短摘要的生成
- 最長公共子序列
- 木塊砌墻原稿
- 附近地點搜索
- 隨機取出其中之一元素