1. 定義
紅黑樹也是二叉查詢樹,我們知道,二叉查詢樹這一資料結構並不難,而紅黑樹之所以難是難在它是自平衡的二叉查詢樹,在進行插入和刪除等可能會破壞樹的平衡的操作時,需要重新自處理達到平衡狀態。紅黑樹是一種含有紅黑結點並能自平衡的二叉查詢樹,又稱黑色完美平衡。
動畫演示:https://rbtree.phpisfuture.com/
2. 節點稱呼
3. 性質
-
每個節點要麼是黑色,要麼是紅色。
-
根節點一定是黑色。
-
每個葉子節點(nil或null)都是黑色的。
-
每個紅節點的兩個子節點一定是黑色的。(不可以同時存在兩個相連的紅結點,即:紅節點的父結點與子結點都是黑的)
-
從任意節點出發到每個葉子節點的路徑都包含相同個數的黑色節點。
* 如果一個結點存在黑子結點,那麼該結點肯定有兩個子結點。 * 黑色完美平衡。
下面是一棵簡單的紅黑樹,Nil(java中為null)是葉子節點併為黑色:
上圖中的紅黑樹並不是完美平衡的二叉查詢樹,P節點的左邊比右邊高,但是左右黑色的層數是相等的,任意一個結點到葉子節點的黑色節點數都相同(性質5),也被成為黑色完美平衡。
4. 紅黑樹的自平衡
4.1 左旋
以某個結點作為支點(旋轉結點),其右子結點變為旋轉結點的父結點,右子結點的左子結點變為旋轉結點的右子結點,其他結點保持不變。
4.2 右旋
以某個結點作為支點(旋轉結點),其左子結點變為旋轉結點的父結點,左子結點的右子結點變為旋轉結點的左子結點,其他結點保持不變。
4.3 變色
結點的顏色由紅變黑或由黑變紅。
5. 紅黑樹的查詢
紅黑樹是一顆二叉平衡樹,查詢不會破壞平衡性,所以和二叉平衡術查詢方式一致。
- 從根節點開始查詢,為空就返回null,為當前值就返回,否則繼續向下查詢。
- 如果當前節點的key為要查詢的節點的key,那麼直接返回當前值。
- 如果當前節點的key大於要查詢的節點的key,那麼繼續向當前節點的左子節點查詢。
- 如果當前節點的key小於要查詢的節點的key,那麼繼續向當前節點的右子節點查詢。
6. 紅黑樹的插入
插入會破壞紅黑樹的黑色完美平衡,所以插入第一步要找到要插入的位置進行插入,第二步進行自平衡。
6.1 查詢插入位置
所有插入操作都是在葉子結點進行的。
- 插入節點的顏色肯定為紅色。因為插入節點為黑色,就會破壞黑色完美平衡,使得到葉子節點的黑色數+1,而紅色不會破壞。
- 基本與紅黑樹的查詢相同:
從根節點開始,如果根節點為空,則插入在根節點,否則根節點為當前節點。
- 如果當前節點為null,則返回當前節點的父節點進行插入。
- 如果當前節點的key等與插入節點的key,則更新當前節點的value。
- 如果當前節點的key大於插入節點的key,則繼續向當前節點的左子節點繼續查詢。
- 如果當前節點的key小於插入節點的key,則繼續向當前節點的右子節點繼續查詢。
6.2 插入的自平衡
插入主要指標指向插入結點,通過4. 紅黑樹的自平衡將紅黑樹達到的平衡即可
左旋
條件:當前節點的父節點是紅色 & 當前節點的叔叔節點是黑色或者不存在 & 當前結點是其父節點的右子結點。
步驟:
- 將父節點左旋
- 將指標指向父結點
右旋
條件:當前節點的父節點是紅色 & 當前節點的叔叔節點是黑色或者不存在 & 當前結點是其父節點的左子結點。
步驟:
- 將父節點變為黑色
- 將祖父結點變為紅色
- 將祖父結點右旋
- 將指標指向祖父結點
變色
條件:當前節點的父節點是紅色並且當前節點的叔叔節點也是紅色。
步驟:
- 當前結點是根結點直接變為黑色
- 當前結點不是根結點
- 將父節點與叔叔節點變為黑色
- 將祖父結點變為紅色
- 將指標指向祖父結點
JDK1.8中插入自平衡的原始碼實現:
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)))) {
// y是插入節點的祖父節點的右子節點(叔叔節點)
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
// y是紅色
if (colorOf(y) == RED) {
// 變色處理
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
// 指標指向插入節點的祖父節點
x = parentOf(parentOf(x));
} else {
// y是黑色的
// 插入節點是是父節點的右子節點
if (x == rightOf(parentOf(x))) {
// 父節點左旋
x = parentOf(x);
rotateLeft(x);
}
// 插入節點是是父節點的左節點
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
// 祖父節點右旋
rotateRight(parentOf(parentOf(x)));
}
} else {
// 插入的父節點是右子節點
// y是插入節點的祖父節點的左子節點(叔叔節點)
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
// y是紅色
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;
}
7. 紅黑樹刪除
刪除操作與插入差不多,查詢、刪除、自平衡。查詢目標結點顯然可以複用查詢操作,當不存在目標結點時,忽略本次操作;當存在目標結點時,刪除後就得做自平衡處理了。刪除了結點後我們還需要找結點來替代刪除結點的位置,不然子樹跟父輩結點斷開了,除非刪除結點剛好沒子結點,那麼就不需要替代。
7.1 查詢刪除位置
基本與紅黑樹的查詢相同:
- 從根節點開始,如果根節點為空,則刪除在根節點,否則根節點為當前節點。
- 如果當前節點為null,則返回當前節點的父節點進行插入。
- 如果當前節點的key等與刪除節點的key,則找到當前節點。
- 如果當前節點的key大於刪除節點的key,則繼續向當前節點的左子節點繼續查詢。
- 如果當前節點的key小於刪除節點的key,則繼續向當前節點的右子節點繼續查詢。
7.2 刪除結點
刪除節點的可能情況:
JDK1.8中TreeMap刪除可能性原始碼實現:
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
// 如果刪除節點有兩個子節點
if (p.left != null && p.right != null) {
// 找到替代節點(很簡單,自己看TreeMap原始碼)
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
// 如果有一個替換節點
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
// 如果存在替換節點
if (replacement != null) {
// Link replacement to parent
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 out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
}
// 如果刪除節點是根節點
else if (p.parent == null) { // return if we are the only node.
root = null;
} else {
// 沒有子節點
if (p.color == BLACK)
fixAfterDeletion(p);
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;
}
}
}
7.3 刪除後的自平衡
刪除自平衡處理:
JDK1.8中TreeMap刪除自平衡原始碼實現:
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
// 刪除節點是左子節點
if (x == leftOf(parentOf(x))) {
// sib是刪除節點父節點的右子節點(兄弟節點)
Entry<K,V> sib = rightOf(parentOf(x));
// 兄弟節點是紅色
if (colorOf(sib) == RED) {
// 情況1.1處理
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
// sib兄弟節點有兩個黑色的子節點,情況2處理
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
// 變色
setColor(sib, RED);
// 指標指向刪除節點的父節點
x = parentOf(x);
} else {
// 兄弟節點的右子節點是黑色
if (colorOf(rightOf(sib)) == BLACK) {
// 情況3.1.1處理
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
// 情況3.1.2處理
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
// 跳出迴圈
x = root;
}
} else { // symmetric
// 刪除節點是右子節點
// sib是刪除節點父節點的左子節點(兄弟節點)
Entry<K,V> sib = leftOf(parentOf(x));
// 兄弟節點是紅色
if (colorOf(sib) == RED) {
// 情況1.2處理
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
// sib兄弟節點有兩個黑色的子節點,情況2處理
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
// 變色
setColor(sib, RED);
// 指標指向刪除節點的父節點
x = parentOf(x);
} else {
// 兄弟節點的左子節點是黑色
if (colorOf(leftOf(sib)) == BLACK) {
// 情況3.2.1處理
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
// 情況3.2.2處理
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
// 跳出迴圈
x = root;
}
}
}
setColor(x, BLACK);
}
參考
結語
歡迎關注微信公眾號『碼仔zonE』,專注於分享Java、雲端計算相關內容,包括SpringBoot、SpringCloud、微服務、Docker、Kubernetes、Python等領域相關技術乾貨,期待與您相遇!