1.概念
紅黑樹(R-B Tree), 全稱Red-Block Tree. 它是一種特殊的二叉樹, 樹中的每個節點都有顏色, 可以是紅可以是黑.
注意: 非紅色節點就是黑色節點, 即NULL節點是黑色節點
學習時可以先按照既定的規則進行調整, 學會后, 再去思考為什么有這些情況, 然后再考慮為什么這種情況需要這么處理.
2.性質
- 性質1: 節點是紅色或黑色.
- 性質2: 根節點是黑色.
- 性質3: 每個NULL節點是黑色.
- 性質4: 每個紅色節點的兩個孩子節點一定是黑色.
- 性質5: 從任意節點到其NULL節點的所有路徑中都包含相同數目的黑色節點.
3.預備知識-旋轉
當紅黑樹的結構發生改變時(添加/刪除元素), 紅黑樹的性質可能會被破壞, 需要通過調整使樹重新成為紅黑樹, 調整可以分為兩類:
- 顏色調整: 改變節點的顏色
- 結構調整: 左旋 + 右旋
3-1.左旋
左旋要確定對誰(旋轉節點)進行左旋.
簡單說: 左旋就是把旋轉節點變為其右孩子的左節點(右孩子變為旋轉節點的父節點).
3-1-1.左旋步驟
- 將旋轉節點的右節點的左節點指向旋轉節點的右節點上(雙向關聯).
- 將旋轉節點的右節點的父節點指向旋轉節點的父節點(雙向關聯).
- 將旋轉節點的父節點指向旋轉節點的右節點(雙向關聯).
3-1-2.左旋示例圖
假設旋轉節點為: 節點20, 對旋轉節點進行左旋. 如下圖
3-1-3.參考TreeMap的左旋代碼
/** From CLR */
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
// 獲取p的右節點r, 臨時存儲
Entry<K,V> r = p.right;
// --將旋轉節點的右節點的左節點指向旋轉節點的右節點上(雙向關聯).
// 將p的右節點的左節點連接到p的右節點上
p.right = r.left;
// 將p的右節點的左節點的父節點指向為p
if (r.left != null)
r.left.parent = p;
// 將旋轉節點的右節點的父節點指向旋轉節點的父節點(雙向關聯).
// 將p的父節點賦值給r, r的父節點指向為p的父節點
r.parent = p.parent;
if (p.parent == null) // 父節點為空, 根節點即為 r
root = r;
else if (p.parent.left == p) // p是父節點的左節點
p.parent.left = r;
else // p是父節點的右節點
p.parent.right = r;
// 將旋轉節點的父節點指向旋轉節點的右節點(雙向關聯).
r.left = p;
p.parent = r;
}
}
3-2.右旋
右旋要確定對誰(旋轉節點)進行右旋.
簡單說: 右旋就是把旋轉節點變為其左孩子的右節點(左孩子變為旋轉節點的父節點).
3-2-1.右旋步驟
- 將旋轉節點的左節點的右節點指向旋轉節點的左節點上(雙向關聯).
- 將旋轉節點的左節點的父節點指向旋轉節點的父節點(雙向關聯).
- 將旋轉節點的父節點指向旋轉節點的左節點(雙向關聯).
3-2-2.右旋示例圖
假設旋轉節點為: 節點30, 對旋轉節點進行右旋. 如下圖
3-2-3.參考TreeMap的右旋代碼
/** From CLR */
private void rotateRight(Entry<K,V> p) {
if (p != null) {
// 臨時存儲p的左節點
Entry<K,V> l = p.left;
// 將旋轉節點的左節點的右節點指向旋轉節點的左節點上(雙向關聯).
p.left = l.right;
if (l.right != null)
l.right.parent = p;
// 將旋轉節點的左節點的父節點指向旋轉節點的父節點(雙向關聯).
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else p.parent.left = l;
// 將旋轉節點的父節點指向旋轉節點的左節點(雙向關聯).
l.right = p;
p.parent = l;
}
}
4.預備知識-尋找節點的后繼
當節點元素被刪除時, 如果待刪除節點有兩個孩子, 則不能刪除該節點, 應該尋找到待刪除節點的前驅或后繼節點, 然后使用前驅或后繼節點中值覆蓋待刪除節點的值, 最后把前驅或后繼節點刪除.
實際上節點的后繼節點就是紅黑樹按照中序遍歷結果, 節點元素的后一個元素, 前驅節點同理.
理解了二叉樹的中序遍歷, 這里邊很容易理解了.
參考TreeMap的尋找后繼代碼:
/**
* Returns the successor of the specified Entry, or null if no such.
*/
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null) // null is null
return null;
else if (t.right != null) { // 右節點非空
// 循環尋找右節點的左節點的左節點..., 直到左節點的左節點為null, 返回.
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else { // 右節點為null
// t是父節點的右節點: 一直獲取父節點, 直到獲取到根節點, 返回
// t是父節點的左節點: 后繼節點就是父節點, 返回
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
當然TreeMap中還有尋找節點的前驅的方法: Entry<K,V> predecessor(Entry<K,V> t)
.
5.插入調整
紅黑樹的插入操作如同二叉排序樹的插入操作一樣, 不同的時, 在新元素插入之后, 需要對數進行調整使其重新成為一顆紅黑樹, 這里就研究如何進行調整.
新插入的元素一定是葉節點, 那么父節點為黑色就不需要進行處理, 因為新插入的元素默認染為紅色, 如果父節點是紅色, 就違反了性質4, 此時需要進行調整.
5-1.插入新元素時會出現的情況
- 情況1: 紅黑樹是空樹
- 情況2: 父節點為黑色
- 情況3: 父節點為紅色 & 叔叔節點為紅色
- 情況4: 父節點為紅色 & 叔叔節點為黑色
5-2.情況1: 紅黑樹是空樹
處理步驟:
- 將新節點染為紅色
- 將新節點染為黑色
調整完成.
示例圖
5-3.情況2: 父節點為黑色
父節點是黑色, 添加一個紅色孩子節點並不會影響紅黑樹的性質, 不需要調整.
示例圖
在只有根節點(20)的紅黑樹中插入一個新節點(10), 如下圖
大家可以嘗試一下在復雜的樹中插入, 也不會影響紅黑樹的性質的.
5-4.情況3: 父節點為紅色 & 叔叔節點為紅色
處理步驟:
- 將父節點和叔叔節點染為黑色
- 將祖父節點染為紅色
按照上述步驟調整之后, 祖父節點的顏色由黑色變為紅色, 這時需要對祖父節點進行調整.
示例圖
在現有的紅黑樹中插入新節點(35), 則調整過程如下
圖中祖父節點即為根節點, 直接染為黑色即可, 如果祖父節點非根節點, 此時需要將當前節點指向祖父節點, 對祖父節點進行進一步的調整.
5-5.情況4: 父節點為紅色 & 叔叔節點為黑色
處理步驟(父節點是祖父節點的左節點):
- 將新節點調整為父節點的左孩子節點(如果是父節點的右孩子的話)
- 將父節點作為新節點
- 對新節點進行左旋
- 將父節點染為黑色
- 將祖父節點染為紅色
- 對祖父節點進行右旋
處理步驟(父節點是祖父節點的右節點):
- 將新節點調整為父節點的右孩子節點(如果是父節點的左孩子的話)
- 將父節點作為新節點
- 對新節點進行右旋
- 將父節點染為黑色
- 將祖父節點染為紅色
- 對祖父節點進行左旋
發現, 節點的調整只在以祖父節點為根的樹中進行調整, 調整前后祖父節點的顏色不變, 只要把以祖父節點為根的樹調整為紅黑樹即可. 但是要保證調整前與調整后, 從祖父節點從葉節點的路徑要包含相同數目的黑節點.
所以, 經過該步驟調整之后, 樹一定為紅黑樹.
示例圖
在現有的紅黑樹中插入新節點(45), 則調整過程如下
調整完成.
5-6.插入調整總結
插入調整總體看起來比較簡單, 閉眼冥想一下各種情況, 然后接下來看代碼.
5-7.參考TreeMap的插入調整代碼
/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
// 默認新節點的顏色為紅色, 默認紅色處理起來比較簡單, WHY?
x.color = RED;
// 父節點為紅色時, 增加一個新節點, 會違反性質4
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 父節點為祖父節點的左節點
// 獲取叔叔節點
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) { // 叔叔節點為紅色時
// 父節點和兄弟節點染為黑色
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
// 祖父節點染為紅色
setColor(parentOf(parentOf(x)), RED);
// 當前節點指向為祖父節點
// 如果此時x=root了, 那么方法的最后一行代碼便很有必要了.
x = parentOf(parentOf(x));
} else { // 叔叔節點為黑色時
// 將新節點調整為父節點的左孩子節點
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
// 父節點染為黑色
setColor(parentOf(x), BLACK);
// 祖父節點染為紅色
setColor(parentOf(parentOf(x)), RED);
// 對祖父節點進行右旋
rotateRight(parentOf(parentOf(x)));
// 此時, x的父節點已經被染為黑色了, 退出while循環
}
} else { // 與上面對應
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 最后將根節點染為黑色
root.color = BLACK;
}
6.刪除調整
刪除, 對於紅黑樹來說是最復雜的, 也比較難理解, 分情況進行分析, 就簡單了.
Let's go!
6-1.刪除節點時會出現的情況
- 情況1: 節點既有左子樹又有右子樹
- 情況2: 節點只有左子樹或只有右子樹
- 情況3: 節點既沒有左子樹又沒有右子樹(節點是葉節點)
對於情況1, 我們首先要找到該節點的前驅或后繼節點, 使用前驅或后繼節點的值覆蓋待刪除節點的值, 然后將前驅或后繼節點按照情況2或情況3進行刪除即可, 此時前驅或者后繼節點頂多有一個子節點. 本文中使用后繼.
所以, 對於紅黑樹來說, 實際刪除節點的情況只有兩種(情況2和情況3), 但是這兩種情況又分為多種情況, 下面一一列舉.
下文中, 待刪除節點用節點D(Delete)表示.
6-2.情況2出現的情況
- 情況2-1: 節點D是紅色 & 其右節點(R)是黑色 -- 不存在
- 情況2-2: 節點D是紅色 & 其右節點(R)是紅色 -- 不存在
- 情況2-3: 節點D是紅色 & 其左節點(L)是黑色 -- 不存在
- 情況2-4: 節點D是紅色 & 其左節點(L)是紅色 -- 不存在
- 情況2-5: 節點D是黑色 & 其右節點(R)是黑色 -- 不存在
- 情況2-6: 節點D是黑色 & 其右節點(R)是紅色
- 情況2-7: 節點D是黑色 & 其左節點(L)是黑色 -- 不存在
- 情況2-8: 節點D是黑色 & 其左節點(L)是紅色
分析情況2, 只會存在情況2-6和情況2-8的刪除, 其它情況並不符合紅黑樹的特性, 所以根本不會存在其它情況的刪除, 再看情況2-6和情況2-8, 由於節點D頂多有一個孩子, 所以兩種情況的處理方式是一樣的.
6-2-1.情況2-6: 節點D是黑色 & 其右節點(R)是紅色
處理步驟:
- 將其右節點鏈接到其父節點上.
- 將其右節點染為黑色即可.
等同於刪除了一個紅色節點, 並不影響紅黑樹的性質.
示例圖
在現有的紅黑樹中刪除節點30, 過程如下
此時, 只需把節點刪除, 然后把其后繼節點染為黑色即可.
6-2-2.情況2-8: 節點D是黑色 & 其左節點(L)是紅色
處理步驟:
- 將其左節點鏈接到其父節點上.
- 將其左節點染為黑色即可.
等同於刪除了一個紅色節點, 並不影響紅黑樹的性質.
示例圖
如同情況2-6的示例圖, 只不過孩子節點在左邊而已.
6-3.情況3出現的情況
- 情況3-1: 節點D是紅色
- 情況3-2: 節點D是黑色 & 兄弟節點是紅色
- 情況3-3: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點有孩子節點
- 情況3-3: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點無孩子節點
6-3-1.情況3-1: 節點D是紅色
此時父節點一定為黑色, 如果有兄弟節點, 兄弟節點一定為紅色.
示例圖
在現有的紅黑樹中刪除節點35, 過程如下
刪除一個紅色節點並不會影響紅黑樹的性質, 無須調整.
6-3-2.情況3-2: 節點D是黑色 & 兄弟節點是紅色
此時, 兄弟節點一定有兩個黑色子節點, 因為節點D是葉節點.
處理步驟(節點D是父節點的左節點):
- 父節點染為紅色
- 兄弟節點染為黑色
- 對父節點進行左旋
- 重新計算兄弟節點
處理步驟(節點D是父節點的右節點):
- 父節點染為紅色
- 兄弟節點染為黑色
- 對父節點進行右旋
- 重新計算兄弟節點
示例圖
在現有的紅黑樹中刪除節點10, 過程如下
虛線表示節點被刪除了, 經過該步驟之后, 樹還不是紅黑樹, 需要進一步調整.
6-3-3.情況3-3: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點的子節點至少一個為紅色
兄弟節點的子節點包括孩子節點和NULL節點, 因為它們都是黑色.
處理步驟(節點D是父節點的左節點):
- 將兄弟節點的紅色子節點調整為兄弟節點的右節點(如果兄弟節點的右節點是黑色)
- 將兄弟節點的左節點染為黑色
- 將兄弟節點染為紅色
- 對兄弟節點進行右旋
- 重新計算兄弟節點
- 將兄弟節點的顏色染為父節點的顏色
- 將父節點染為黑色
- 將兄弟節點的右節點染為黑色
- 對父節點進行左旋
處理步驟(節點D是父節點的右節點):
- 將兄弟節點的紅色子節點調整為兄弟節點的左節點(如果兄弟節點的左節點是黑色)
- 將兄弟節點的右節點染為黑色
- 將兄弟節點染為紅色
- 對兄弟節點進行左旋
- 重新計算兄弟節點
- 將兄弟節點的顏色染為父節點的顏色
- 將父節點染為黑色
- 將兄弟節點的右節點染為黑色
- 對父節點進行右旋
如果兄弟節點有兩個紅色子節點, 直接從第2步開始調整, 如果兄弟節點有一個紅色子節點, 需要先將紅色子節點調整為與兄弟節點方向一致的位置.
發現, 節點的調整只在以父節點為根的樹中進行調整, 調整前后父節點的顏色不變, 只要把以父節點為根的樹調整為紅黑樹即可. 但是要保證調整前與調整后, 從父節點從葉節點的路徑要包含相同數目的黑節點.
所以, 經過該步驟調整之后, 樹一定為紅黑樹.
示例圖
在現有的紅黑樹中刪除節點25, 過程如下
調整完之后便是紅黑樹.
6-3-4.情況3-4: 節點D是黑色 & 兄弟節點是黑色 & 兄弟節點的子節點都為黑色
兄弟節點的子節點包括孩子節點和NULL節點, 因為它們都是黑色.
處理步驟:
- 將兄弟節點染為紅色
- 將待調整的節點指向父節點
這種情況刪除節點D之后, 如果父節點是紅色, 直接把父節點染為黑色, 兄弟節點染為紅色即可. 如果父節點是黑色, 刪除后經過父節點的路徑少了一個黑節點, 需要對父節點進行調整.
示例圖
在現有的紅黑樹中刪除節點10, 過程如下
圖中的情況是父節點是紅色的情況, 如果父節點是黑色呢? 看下圖
注: D表示待刪除節點; X表示當前節點.
圖中也只是演示了一種情況, 可以會出現其它情況, 但是任何情況也會坐落於刪除的這幾種情況之中.
6-4.刪除調整總結
刪除時, 先看待刪除節點的顏色, 再看其兄弟節點的顏色, 最后看兄弟節點是否有子節點, 根據具體的情況進行調整.
6-5.參考TreeMap的刪除調整代碼
/** From CLR */
private void fixAfterDeletion(Entry<K,V> x) {
// 刪除的節點為黑色時, 需要進行調整
while (x != root && colorOf(x) == BLACK) {
// 當前節點是左節點
if (x == leftOf(parentOf(x))) {
// 獲取右節點(兄弟節點)
Entry<K,V> sib = rightOf(parentOf(x));
// 兄弟節點是紅色時
if (colorOf(sib) == RED) {
// 兄弟節點染為黑色
setColor(sib, BLACK);
// 父節點染為紅色
setColor(parentOf(x), RED);
// 對父節點進行左旋
rotateLeft(parentOf(x));
// 重新計算兄弟節點
sib = rightOf(parentOf(x));
}
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) { // 兄弟節點的兩個子節點都是黑色, 其實是NULL節點
// 兄弟節點染為紅色
setColor(sib, RED);
// 將當前節點指向父節點
x = parentOf(x);
} else { // 兄弟節點的有子節點
// 將兄弟節點的紅色子節點調整為兄弟節點的右節點
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 將兄弟節點的顏色染為父節點的顏色
setColor(sib, colorOf(parentOf(x)));
// 父節點染為黑色
setColor(parentOf(x), BLACK);
// 兄弟節點的右孩子染為黑色
setColor(rightOf(sib), BLACK);
// 對父節點進行左旋
rotateLeft(parentOf(x));
// 調整完成, 退出循環
x = root;
}
} else { // symmetric
Entry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
// 將x染為黑色
setColor(x, BLACK);
}
7.總結
紅黑樹是一個比較重要的算法, 我覺得作為一個程序員應該需要了解它.
紅黑樹的核心在於元素變動之后, 如何進行調整使其重新成為一顆紅黑樹.
通過學習紅黑樹, 深刻體會到大問題並不可怕, 一點點拆分為小問題, 一定會解決的.
如有發現錯誤, 煩請指出.