節點的插入和刪除
我們知道變色和旋轉是為了修正被破壞的紅黑樹,使其符合紅黑樹的規則,從新達到平衡狀態。那么增加或刪除節點在具體情況下該如何操作呢?
插入節點
紅黑樹的節點插入與二叉查找樹的插入的過程是一樣的,只是最后多了一步平衡調整操作。
新插入的節點默認為紅色節點,所以新節點插入到黑色節點下時不需要對平衡調整,當插入到紅色節點下時才需要調整,因為插入到紅色節點下違反了兩個紅色節點不能相鄰規則。
插入節點的平衡調整有如下幾種情況:
- 父節點的兄弟節點(也就是叔叔節點)為紅色
- 父節點的兄弟節點為NULL節點(NULL默認為黑色),且祖父節點、父節點和新節點在一條直線
- 父節點的兄弟節點為NULL節點,且祖父節點、父節點和新節點不在一條直線
Case 1:
新插入節點的父節點是紅色,其父節點的兄弟節點也是紅色時,通過變色操作調整平衡。
此時將父節點和父節點的兄弟節點(叔叔節點)變為黑色,將祖父節點(父節點的父節點)變為紅色,接下來以祖父節點為基礎繼續向上驗證修復,但最后根節點必須為黑色。
如圖: 依次插入節點[200,100,300,50]
Case 2:
新插入節點的父節點為紅色,父節點的兄弟節點為黑色時,且父節點、祖父節點和新節點3個節點在同一方向上(一條直線),如果為偏左的一條直線(如:和這個“/”方向一致),此時需要右旋(對祖父節點)。
如果為偏右的一條直線(如:和這個“\”方向一致),此時需要左旋。然后新節點的父節點變為黑色,其兄弟節點(原始的祖父節點)變為紅色。
如圖:依次插入節點[200,100,300,50,20] - 以右旋情況為例
Case 3:
該種情況需要旋轉2次。
新插入節點的父節點為紅色,父節點的兄弟節點為黑色時,且父節點、祖父節點和新節點3個節點不在同一方向上(不是一條直線),如果新節點是父節點的右子節點(此時,父節點在祖父節點的左側,此時這三個節點構成這樣“<”方向非直線),此時需要先左旋(變為一條直線)再右旋,兩次旋轉。
如果新節點是父節點的左子節點(此時,父節點在祖父節點的右側,此時這三個節點構成這樣“>”方向非直線),此時需要先右旋(變為一條直線)再左旋,兩次旋轉。
可以發現,經過第一次旋轉后,其實是被轉變成了Case 2 的情況。
如圖: 依次插入節點[200,100,300,50,80] - 以先左再右旋為例
偽代碼- 插入元素平衡修正(來源Java TreeMap):
void fixAfterInsertion(Node<K,V> x) {
// 新插入節點默認紅色
x.color = RED;
// 不是根節點且父節點為紅色
while (x != null && x != root && x.parent.color == RED) {
//父節點在祖父節點的左側
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 叔叔節點
Node<K,V> y = rightOf(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 == rightOf(parentOf(x))) {
x = parentOf(x);
// 左旋
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
// 右旋
rotateRight(parentOf(parentOf(x)));
}
} else { // 對稱的
Node<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;
}
刪除節點
紅黑樹的節點刪除和二叉搜索樹的節點刪除過程是一樣的,只是最后多了一步平衡糾正操作。
只有當刪除黑色節點時需要平衡調整(刪除紅色節點時不需要平衡調整,沒破壞紅黑樹規則),因刪除黑色節點后,會違反從任意節點到葉子節點的每條路徑包含的黑色節點數目相同的規則。
刪除節點要比插入節點復雜些,插入節點整個過程是先找到插入節點位置 -> 插入節點 -> 平衡調整。而刪除節點整個過程是先找到刪除節點的位置 -> 節點的位置變換 -> 刪除節點 -> 平衡調整,且調整的情況也相對較多。
前面說過,紅黑樹與二叉搜索樹的刪除節點過程一樣,當刪除節點存在兩個子節點,會被轉換為刪除有一個子節點或不存在子節點的節點。
對於有一個子節點的節點刪除,比較簡單,不需要平衡調整,只需要將其子節點改為黑色即可。
如圖: 依次插入節點[200,100,300,50]
對於刪除不存在子節點的節點,這種情況最為復雜,需要平衡調整。
刪除節點平衡調整有如下幾種情況:
- 待刪除節點的兄弟節點是紅色
- 待刪除節點的兄弟節點是黑色,且兄弟節點的子節點都是黑色
- 待刪除節點的兄弟節點是黑色,如果兄弟節點在右側,且兄弟節點的左子節點是紅色,右子節點是黑色。如果兄弟節點在左側,就是兄弟節點的右子節點是紅色,左節點是黑色
- 待刪除節點的兄弟節點是黑色,如果兄弟節點在右側,且兄弟節點的右子節點是紅色,左子節點是任意顏色。如果兄弟節點在左側,則就是兄弟節點的左子節點是紅色,右子節點是任意色
Case 1:
對於 Case 1,首先改變父節點和兄弟節點顏色(兄弟節點變為黑色,父節點變為紅色),再對父節點做一次旋轉(紅色的兄弟節點在右側,左旋。紅色的兄弟節點在左側,右旋),操作后,紅黑的規則沒有被破壞,樹的黑色高度沒變化,原兄弟節點的一個子節點變成刪除節點的新兄弟節點。
所以,Case 1 轉化成了 Case 2 或 Case 3、Case 4 幾種情況。
如圖: 依次插入節點[200,100,300,400,500,600] - Case1 轉換為 Case2,3,4 等情況
Case 2:
對於Case 2,刪除節點后其父節點的左子樹比右子樹黑高度小1,此時需要把兄弟節點改為紅色,則左右子樹黑高度就相同了。此時將刪除節點的父節點變為新的節點,然后繼續向上迭代調整,修正平衡。
如圖: 依次插入節點[200,100,300,400,刪除400] 得到下圖結構
Case 3:
對於Case 3,兄弟節點在右側時,交換兄弟節點和其左子節點(紅色)的顏色,並對兄弟節點進行右旋,於是被刪除節點的新兄弟是一個有紅色右子節點的黑色節點。相反,兄弟節點在左側時,交換兄弟節點和其右子節點(紅色)的顏色,並對兄弟節點進行左旋,於是被刪除節點的新兄弟是一個有紅色左子節點的黑色節點。
所以,Case 3 轉化成了Case 4。
如圖: 依次插入節點[200,100,300,250]
Case 4:
對於Case 4,兄弟節點在右側時,兄弟節點和父節點交換顏色,把兄弟節點的右子節點設為黑色,並對刪除節點的父節點做一次左旋轉。
兄弟節點在左側時,兄弟節點和父節點交換顏色,把兄弟節點的左子節點設為黑色,並對刪除節點的父節點做一次右旋轉。
最后將刪除節點直接指向根節點,循環結束。
如圖: 依次插入節點[200,100,300,400]
偽代碼 - 刪除元素平衡修正(來源Java TreeMap):
void fixAfterDeletion(Node<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Node<K,V> sib = rightOf(parentOf(x));
// Case 1
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
// Case 2
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
// Case 3
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// Case 4
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // 對稱的
Node<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的TreeMap集合(略有改動),這是個權威的參考。
為何很多應用選擇紅黑樹作為平衡樹的實現
插入或刪除節點時,平衡二叉樹為了維持平衡,需要對其進行平衡糾正,這需要較大的開銷。
例如,有種AVL樹, 是高度平衡樹,結構比紅黑樹更加平衡,這也表示其增刪節點時也更容易失衡,失衡就需要糾正,增刪節點時開銷會比紅黑樹大。但不可否認的是AVL樹搜索的效率是非常穩定的。
因此在大量數據需要插入或者刪除時,AVL需要平衡調整的頻率會更高。因此,紅黑樹在需要大量插入和刪除節點的場景下,效率更高。自然,由於AVL高度平衡,因此AVL的Search效率略高。
紅黑樹不是高度平衡樹,但平衡的效果已經很好了。所以,可以認為紅黑樹是一種折中的選擇,其綜合性能較好。
最后
紅黑樹的介紹到此結束,學習過程中可以參考 TreeMap 源碼來學習,會有意想不到的效果!
推薦:
數據結構之紅黑樹-動圖演示(上)
參考:
JDK TreeMap源碼
https://www.zhihu.com/question/20545708