csdn 連接:blog.csdn.net/ziwang_/art…java
注:本文的源碼摘自 jdk1.8 中 TreeMap數組
如下有幾個違反上述規則的結點示例:bash
結點必須是紅色或黑色學習
根結點必須是黑色的ui
葉子結點必須是黑色的spa
以上三個都是錯誤的紅黑樹示例,每一個紅色結點的兩個子結點都是黑色,而以下是合格的.net
固然,細心的讀者應該發現了我只是展現了前四條性質而沒有展現第五條性質,沒有什麼理由,筆者就是懶,第五條挺好理解的。翻譯
這裏的左旋右旋都是針對根節點而言的,因此左圖到右圖是 y 結點右旋,右圖到左圖是 x 結點左旋。設計
如今不理解這倆概念有什麼用不重要,可是但願讀者能理解它的變幻過程,到後面會涉及到。3d
提及來枯燥無心,咱們能夠結合 TreeMap 來看看左旋右旋的源碼 ——
在這裏咱們就針對左旋源碼看看 ——
筆者就直接一行一行解釋吧:
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right; // r 是根結點右子結點
p.right = r.left; // 爲根結點的左結點指向右子結點(也就是 r)的左結點
if (r.left != null)
r.left.parent = p; // 意義同第二步,這步是右子結點(也就是 r)的左結點將父結點引用指向 p
r.parent = p.parent; // 將 r 結點的父引用指向 p 結點的父引用
if (p.parent == null)
root = r; // 將根結點替換爲 r
else if (p.parent.left == p)
p.parent.left = r; // 意義同上
else
p.parent.right = r; // 意義同上
r.left = p; // r 左結點引用指向 p 結點
p.parent = r; // p 結點父引用指向 r 結點
}
}複製代碼
設置成黑色的吧,就違反了性質5,設置成了紅色的吧,就容易違反了性質4。那怎麼辦?總要給一個顏色,那咱們就給紅色的吧。爲何?由於若是設置成黑色的話,該分支的黑色結點數量確定比其餘分支多一個,而這樣的話至關地很差作調整。若是將插入結點顏色置爲紅色的話,運氣比較好的狀況下該父結點就是黑色的,那這樣就不須要作任何調整。另外一種狀況是插入結點的父結點顏色是紅色的,這種狀況咱們就須要詳細討論了,具體分爲如下兩種(此處咱們以插入結點的父結點是爺爺結點的左子結點爲例(有點拗口),鏡像操做道理相同):
父結點與叔叔結點都爲紅的話那麼一定爺爺結點爲黑,實際上此時咱們最簡單的操做就是將父結點和叔叔結點染黑,將爺爺結點染紅(將爺爺結點染紅的目的是爲了保證爺爺結點路徑的黑色結點數量不改變),以下 ——
如今目標結點、父結點、叔叔結點都符合要求了,可是爺爺結點的父結點是紅色的,那麼就衝突了,聰明的讀者可能已經發現了,此時的爺爺結點就至關於目標結點,咱們不妨將爺爺結點置換爲目標結點,再進行遞歸操做就能夠達到解決衝突的目的了。
但凡是有一個結點是紅色,那麼它的父結點一定是黑色(性質4),因此爺爺結點必定是黑色的。
有細心的小夥伴可能覺察到,上圖違反了性質五。實際上上圖是一張簡化後的圖,爲了咱們後面的內容更加便於理解,上圖的原圖應該是如下模樣 ——
ps:上圖中叔叔結點和兄弟結點能夠理解成 java 中的 null 結點,筆者特意將它們的個頭縮小了,以便區分。
那麼此時該怎麼操做呢?爺爺結點右旋,爺爺結點置紅,父結點置黑。這條操做事後,性質四、5都沒有違反。
固然,上圖也只是一張簡化圖,實際上原圖以下:
那麼結合 TreeMap 源碼咱們來看看:
翻譯以下:
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED; // 目標結點顏色賦紅
// 目標結點非空,非根,同時父結點爲紅,此時才須要調整
while (x != null && x != root && x.parent.color == RED) {
// 父結點是爺爺的左子結點
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x))); // y 是叔叔結點
// 狀況1 叔叔結點也爲紅
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK); // 父結點賦黑
setColor(y, BLACK); // 叔叔結點賦黑
setColor(parentOf(parentOf(x)), RED); // 爺爺結點賦紅
x = parentOf(parentOf(x)); // 爺爺結點置爲目標結點,遞歸
} else {
// 狀況2 叔叔結點爲黑
// 小插曲,若是目標結點是父結點的右子結點,左旋父結點
// 固然,此時目標結點應改成父結點
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK); // 父結點賦黑
setColor(parentOf(parentOf(x)), RED); // 爺爺結點賦紅
rotateRight(parentOf(parentOf(x))); // 爺爺結點右旋
}
} 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; // 根結點必須賦黑
}複製代碼
看完代碼咱們發現咱們好像漏了一個小插曲(固然,這是筆者故意的),那麼小插曲是一個什麼狀況呢?言語來講,在叔叔結點爲黑的前提下,當目標結點是父結點的右子結點的時候,須要對父結點進行左旋而後才能接續下一步操做,爲何會這樣,咱們一圖勝千言 ——
若是忽略上述狀況,那麼最終會獲得如下狀況:
因爲目標結點是父結點的右子節點,在爺爺結點右旋過程當中,它會轉爲原爺爺結點的左子結點,這樣的話就違反了特性4和特性5。解決方法就是上面所提到的將父結點先進行左旋而後再進行前面所提到的操做,以下圖 ——
固然,不要忘了,如今須要調整的結點是原父結點,也就是要將上圖左下角那個結點做爲目標結點進行調整。
因此紅黑樹的添操做分爲如下三步:
翻譯以下:
private void deleteEntry(Entry<K,V> p) {
// 優先選擇左子結點做爲被刪結點的替代結點
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
// 若是替代結點不爲空
if (replacement != null) {
replacement.parent = p.parent;
// 若是刪除結點爲根節點,那麼根節點重定向引用指向替代結點
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
// 若是刪除結點是其父結點的左子結點,更改父結點左子結點引用指向替代結點
p.parent.left = replacement;
else
// 若是刪除結點是其父結點的右子結點,更改父結點右子結點引用指向替代結點
p.parent.right = replacement;
// 將刪除結點的各個引用置 null
p.left = p.right = p.parent = null;
// 若是刪除結點顏色爲黑色,那麼須要進行刪後調整
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) {
// 若是替代結點爲空且刪除結點爲 root 結點
root = null;
} else {
// 若是刪除結點爲空且不是 root 結點
// 若是刪除結點顏色爲黑色,那麼須要進行刪後調整
if (p.color == BLACK)
fixAfterDeletion(p);
// 將刪除結點的各個引用置 null
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}複製代碼
刪除時可能分爲三種狀況,具體的作法也在上述代碼中作了清晰的解釋,筆者在此就不擴展了,細心的讀者可能發現了,上述刪除操做凡是涉及到了刪除結點是黑色的狀況下,都須要調用 fixAfterDeletion()
方法對紅黑樹進行調整。這是由於若是刪除結點是黑色的,當它被刪除後就會違反性質5,因此咱們須要對紅黑樹進行結構調整。
爲了便於理解紅色結點爲何不會影響紅黑樹總體結構,筆者仍是舉了一個例子給各位讀者理解一下,下圖是刪除前:
下圖是刪除後:
實際上紅黑樹是使用如下2點思想來進行調整的(筆者認爲,在分析 fixAfterDeletion()
代碼實現以前,做爲開發者應該去自行思考一下若是咱們做爲源碼設計者,咱們會如何來解決這個問題。) ——
1.給刪除結點的路徑增長一個黑色結點(將兄弟路徑的一個黑色結點移過來)
2.給刪除結點的兄弟路徑減小一個黑色結點(將兄弟路徑的一個紅色結點染黑)
說完思想,咱們討論一下具體刪除操做是如何進行的。紅黑樹在保障刪除結點的兄弟結點爲黑色的狀況下(沒有什麼特殊原因,僅僅是爲了後期好操做),分如下兩點來進行分析:
1.兄弟結點的兩個子結點都是黑色的
2.另外一種狀況(兄弟結點的兩個子結點至多一個黑色的)
對於狀況1來講,紅黑樹採用思想2,將兄弟結點置爲紅色,可是這樣帶來了兩個問題——對於父路徑來講,它與兄弟路徑黑色結點數量不一樣,違反性質5;且若是父結點也是紅色,那麼它勢必與孩子結點衝突,還會違反性質4,以下圖——
下圖示例違反性質5:
下圖示例違反性質5且違反性質4:
對於前一個問題用遞歸的思想來解決,將父親結點置爲目標結點,讓父親結點的兄弟結點也要減小一個黑色結點就能夠了(借鑑思想2);而對於後一個問題,只須要將父結點置黑便可(借鑑思想2)。jdk 中相關實現源碼以下:
while (x != root && colorOf(x) == BLACK) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
}
}
setColor(x, BLACK);複製代碼
前面闡述的是針對狀況1而言,針對於狀況2而言,紅黑樹採用的是思想1,具體作法分爲又得分爲如下兩種小狀況:
對於第一種小狀況,紅黑樹採用如下操做:
1.兄弟結點置父結點顏色(準備謀權篡位)
2.父結點置黑、兄弟結點右結點置黑
3.父結點左旋
該思想不只保證了更新結點後不會衝突(父結點與兄弟結點不衝突,兄弟結點與右子結點不衝突,兄弟結點左子結點與父結點不衝突),而且保證了黑色結點數量不會改變,一圖勝千言——
jdk 中相關源碼以下:
while (x != root && colorOf(x) == BLACK) {
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
setColor(x, BLACK);複製代碼
而對於第二種小狀況,紅黑樹採用如下操做:
1.將兄弟結點的左子結點染黑
2.兄弟結點染紅
3.兄弟結點右旋
實際上細心的讀者發現了,轉換後的結構是等同於第一種小狀況的初始結構,因此接下來就按照第一種小狀況的步驟去變換結構,相關源碼以下:
while (x != root && colorOf(x) == BLACK) {
if (colorOf(rightOf(sib)) == BLACK) { // 狀況2
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 狀況1
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
setColor(x, BLACK);複製代碼
這一塊可能有一些複雜,但記住如下三點核心思想問題就不是很大了:
那麼接下來就是看看 fixAfterDeletion()
的代碼實現了 ——
解釋以下:
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));
// 小插曲1,若是兄弟結點爲紅
// 這步是保障兄弟結點必定爲黑
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) {
setColor(sib, RED); // 兄弟結點置紅
x = parentOf(x); // 重定向目標結點爲父結點
} else {
// 兄弟結點的子結點至多一個是黑色的
// 小插曲2,兄弟結點左子結點爲紅,右子結點爲黑的狀況
// 這步的意義是讓兄弟結點的右子結點的數量多一個
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 { // 鏡像操做
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;
}
}
}
setColor(x, BLACK);
}複製代碼
紅黑樹在 java 中的運用實際上仍是挺多的,例如 TreeSet
的默認底層實現實際上也是 TreeMap
;jdk 8中的 HashMap
實現也由原來的數組+鏈表更改成了數組+鏈表/紅黑樹。