紅黑樹也是一種平衡搜索樹,其具有如下 5 個限定條件:
- 節點要麼爲紅色,要麼爲黑色;
- 樹根爲黑色;
- 外部節點均爲黑色;
- 紅色節點的孩子爲黑色;
- 從任一外部節點到根節點的沿途,黑節點的數目(黑高)相等。
包含 n 個內部節點的紅黑樹的高度不超過 O(logn)。
樹定義
public class RedBlackTree<T extends Comparable<? super T>> {
// 顏色值
private static final int RED = 0;
private static final int BLACK = 1;
// 紅黑樹節點
private static class RedBlackNode<T> {
T data;
RedBlackNode<T> left, right, parent;
int color;
public RedBlackNode(T data, RedBlackNode<T> left, RedBlackNode<T> right, RedBlackNode<T> parent) {
this.data = data;
this.left = left;
this.right = right;
this.parent = parent;
this.color = RED;
}
}
// 樹根
private RedBlackNode<T> root;
// 是否爲黑色節點(外部節點也爲黑色)
private boolean isBlack(RedBlackNode<T> node) {
return (node == null || node.color == BLACK);
}
// 是否爲紅色節點
private boolean isRed(RedBlackNode<T> node) {
return (node != null && node.color == RED);
}
// node 是否爲左孩子
private boolean isLeftChild(RedBlackNode<T> node) {
return (node.parent != null && node.parent.left == node);
}
// node 是否爲右孩子
private boolean isRightChild(RedBlackNode<T> node) {
return (node.parent != null && node.parent.right == node);
}
// 獲取 node 的叔叔節點
private RedBlackNode<T> uncle(RedBlackNode<T> node) {
return (isLeftChild(node.parent) ? node.parent.parent.right : node.parent.parent.left);
}
...
}
搜索節點
// 用於表示搜索結果(作爲內部類)
private static class SearchResult<T> {
RedBlackNode<T> target; // 目標節點
RedBlackNode<T> parent; // 目標節點之父
// getter & setter ...
}
// 查找關鍵碼 e:返回目標節點及其父節點(插入、刪除時有用)
public SearchResult<T> search(T e) {
SearchResult<T> result = new SearchResult<>();
RedBlackNode<T> current = root, parent = null;
while (current != null) {
int compareResult = e.compareTo(current.data);
// 找到了
if (compareResult == 0) {
break;
}
// 記錄父節點,然後:比當前節點小->往左;否則->往右
parent = current;
current = compareResult < 0 ? current.left : current.right;
}
result.target = current;
result.parent = parent;
return result;
}
插入節點
插入節點時,將新插入節點染爲紅色。(黑高不變)
但可能造成如下問題,導致紅黑樹失衡:
- 如果新插入節點爲樹根,則違背 條件2(樹根需爲黑色);
- 如果新插入節點之父爲紅色(“雙紅”問題),則違背 條件4(紅色節點的孩子需爲黑色);
解決 “雙紅” 問題
設新插入節點爲 v,v 之父爲 p,p 之父爲 g ,p 之兄弟、g 的另一個孩子、v 的叔叔 爲 u 。
此時,v 爲紅色,p 爲紅色,g 爲黑色 (g 不可能爲紅色,因爲其孩子節點 p 已經爲紅色 — 條件4)。
以下根據 u 的顏色分兩種情況:
1. u 爲黑色
根據 g、p、v 之間的連接關係有可分爲 4種 情況。
此時, v 的兄弟、 v 的兩個孩子的黑高,均與 u 的黑高相等。
經過適當的重平衡操作之後(如,單旋、雙旋),設子樹的根爲 b,b 的左右孩子分別爲 a 、c 。則將 b 染爲黑色,將 a、c 均染爲紅色。
如此調整之後, 局部子樹的黑高度將復原,這意味着全樹的平衡也必然得以恢復。同時,新子樹的根節點 b爲黑色, 也不致引發新的雙紅現象。 至此,整個插入操作遂告完成。
2. u 爲紅色
同樣,根據 g、p、v 之間的連接關係有可分爲 4種 情況。
此時, u 的左、右孩子非空且均爲黑色,且它們的黑高必與 v 的兄弟以及 v 的兩個孩子的黑高相等。
只需將紅節點 p 和 u 轉爲黑色,黑節點 g 轉爲紅色, v 保持紅色。此時不需旋轉操作。
如此調整之後局部子樹的黑高保持不變。
設 g 之父爲 gg,當 gg 爲黑色時,調整操作完成;當 gg 爲紅色時,再次引發“雙紅”問題。此時,可以等效地將 g 視作新插入的節點,同樣分以上兩類情況如法處置。
綜上,只有在第一種情況時才需做1~2次旋轉;而且一旦旋轉後,修復過程必然隨即完成。故就全樹拓撲結構而言,每次插入後僅涉及常數次調整。
// 插入關鍵碼
public void insert(T e) {
// 先確定目標節點是否存在
SearchResult<T> result = search(e);
// 已存在
if (result.target != null) {
return;
}
// 插入節點
RedBlackNode<T> node = new RedBlackNode<>(e, null, null, parent);
// 建立父節點到新插入節點的連接
RedBlackNode<T> parent = result.parent;
if (parent == null) { // 新插入節點爲樹根
root = node;
} else { // 新插入節點不是樹根
if (e.compareTo(parent.data) < 0) {
parent.left = node;
} else {
parent.right = node;
}
}
// 解決雙紅問題
solveDoubleRed(node);
}
// 解決“雙紅”問題:父子同爲紅色
private void solveDoubleRed(RedBlackNode<T> node) {
// 已至樹根:將其染黑即可
if (node.parent == null) {
root.color = BLACK;
return;
}
// 父節點
RedBlackNode<T> parent = node.parent;
// 父節點爲黑色,不存在“雙紅”
if (isBlack(parent)) {
return;
}
// 父節點爲紅色,則祖父節點必然存在且爲黑色
RedBlackNode<T> grandparent = parent.parent;
// 根據叔叔節點的顏色,分爲 2 種情況處理
RedBlackNode<T> uncle = uncle(node);
// 叔叔節點爲黑色:1~2 次旋轉和 3次染色 即可完成調整
if (isBlack(uncle)) {
RedBlackNode<T> greatgrandparent = grandparent.parent; // 曾祖父節點
RedBlackNode<T> rootOfSubtree; // 重平衡之後,子樹的根
if (greatgrandparent == null) { // 重平衡之後,子樹的根即是整棵樹的樹根
rootOfSubtree = root = rotateAt(node);
} else { // 否則,將重平衡之後的子樹接入,成爲原曾祖父的孩子
if (isLeftChild(grandparent)) {
rootOfSubtree = greatgrandparent.left = rotateAt(node);
} else {
rootOfSubtree = greatgrandparent.right = rotateAt(node);
}
}
rootOfSubtree.color = BLACK;
// rootOfSubtree 的左右孩子爲 node、parent、grandparent 中的兩個,所以必然不爲空
rootOfSubtree.left.color = RED;
rootOfSubtree.right.color = RED;
rootOfSubtree.parent = greatgrandparent;
}
// 若叔叔節點爲紅色,則只需染色,無需旋轉,然後繼續向上解決“雙紅”
// 當遇到上一種情況是,解決之,即調整操作完成。
else {
parent.color = BLACK;
uncle.color = BLACK;
grandparent.color = RED;
// 遞歸解決之
solveDoubleRed(grandparent);
}
}
刪除節點
設,實際被刪除的節點爲 x,節點 x 的接替者(x 的某一個孩子)爲 r 。此時,可以確定 x 最多隻有一個非空孩子(因爲 x 是實際被刪除的節點 — 注意“實際”二字,可見 二叉搜索樹 - Java 實現 - 刪除節點)。
如果 x 爲紅色,則所有條件依然滿足(不影響黑高),不需調整。
如果 x 爲黑色,則 條件4 和 條件5 可能不滿足。此時,需要根據 r 的顏色決定:
- 如果 r 爲紅色,則將其翻轉爲黑色,即可滿足 條件4 和 條件5 。
- 如果 r 爲黑色,則出現“雙黑”問題(x 和 r 均爲黑色),情況複雜。
解決 “雙黑” 問題
當出現“雙黑”問題時,設 x 的兄弟節點爲 s,則 s 必然非空(x 爲黑色非空節點,如果 s 爲空,則 條件5 不滿足 — 黑高不相等)。設 x 之父爲 p,p 和 s 的顏色不確定。以下根據 p 和 s 的顏色不同組合分 4種情況處理。
(1)s 爲黑色,且 s 有紅色孩子,p 不確定顏色
設 s 的一個紅色孩子爲 t 。根據 p、s、t 之間的連接關係,又可分爲 左左、左右、右右、右左 4種情況。此處僅以 左左 爲例(不管是何種情況,均可通過後面的“統一重平衡”操作完成調整):
首先,對 t、s、p 實施重平衡操作;設重平衡之後的子樹的根爲 b,其左右孩子分別爲 a、c 。則然後,將 a、c 均染爲黑色,讓 b 繼承 p 原來的顏色。
經以上處理之後,紅黑樹的所有條件,都在這一局部以及全局得到滿足,故刪除操作遂告完成。
(2)s 爲黑色,且 s 無紅色孩子,p 爲紅色
(另一種情況對稱)
因爲 x 即將被刪除,即 p 的右子樹的黑高將減一,則將 p 的左子樹的黑高也減一,然後然 p 染爲黑色即可(保證黑高不變 — 條件5)。此處,即是將 s 和 p 的顏色對調。
紅黑樹的所有條件都在此局部得以恢復。由於子樹的黑高保持不變,紅故黑樹條件亦必在全局得以恢復,刪除操作即告完成。
(3) s 爲黑色,且 s 無紅色孩子,p 爲黑色
(另一種情況對稱)
因爲 x 即將被刪除,即 p 的右子樹的黑高將減一,則將 p 的左子樹的黑高也減一,即將 s 染爲紅色,p 保持黑色不變。至此,局部子樹 p 滿足所有條件,但因其黑高減一,在全局不一定滿足 條件5 。
此時的狀態可等效地理解爲:節點 p 的一個黑色父節點剛被刪除,且 p 爲接替者,故可遞歸解決“雙黑”問題。
(4) s 爲紅色,則 p 必爲黑色
(另一種情況對稱)
此處,以節點 p 爲軸做一次旋轉,並交換節點 s 與 p 的顏色。局部子樹滿足條件,但這是在 x 還未被刪除的前提下才成立的。
此處之妙在於,它被轉換成了情況(1)或情況(2)。這就意味着, 接下來至多再做一步迭代調整,整個雙黑修正的任務即可大功告成。
綜上,一旦在某步迭代中做過節點的旋轉調整,整個修復過程便會隨即完成。因此與雙紅修正一樣, 雙黑修正的整個過程,也僅涉及常數次的拓撲結構調整操作。
// 找到 root 子樹中關鍵碼最小的節點及其父節點
private SearchResult<T> findMinimum(RedBlackNode<T> root) {
SearchResult<T> result = new SearchResult<>();
RedBlackNode<T> parent = null;
// 一直往左即可找到最小者
if (root != null) {
while (root.left != null) {
parent = root;
root = root.left;
}
}
result.target = root;
result.parent = parent;
return result;
}
// 刪除
public void remove(T e) {
// 先確認目標節點是否存在
SearchResult<T> result = search(e);
// 不存在
if (result.target == null) {
return;
}
RedBlackNode<T> target = result.target; // 待刪除節點
RedBlackNode<T> parent = result.parent; // 待刪除節點之父
// 有左右兩個非空孩子
// 以待刪除節點爲根,找到其右子樹中關鍵碼最小的節點,讓它代替受死
if (target.left != null && target.right != null) {
SearchResult<T> result1 = findMinimum(target.right);
target.data = result1.target.data;
target = result1.target; // 真正被刪除的節點
parent = result1.parent;
}
// 現在,待刪除節點最多隻有一個非空孩子
// 讓該孩子節點繼承父節點的位置
RedBlackNode<T> child = target.left != null ? target.left : target.right;
if (parent != null) { // 待刪除節點不是樹根
if (parent.left == target) {
parent.left = child;
} else {
parent.right = child;
}
} else { // 待刪除節點是樹根
root = child;
}
// 重平衡操作
// 若刪除的節點爲樹根:將新的樹根染爲黑色
if (parent == null) {
root.color = BLACK;
return;
}
// 若`實際`被刪除的節點爲紅色,則其接替者(其孩子)必爲黑色,此時黑高不變,無需調整
// 若`實際`被刪除的節點爲黑色,且若其接替者(其孩子)爲紅色,則令接替者變爲黑色即可
if (isBlack(target) && isRed(child)) {
child.color = BLACK;
return;
}
// 若`實際`被刪除的節點爲黑色,且若其接替者(其孩子)爲黑色,則出現“雙黑”問題
solveDoubleBlack(child, parent);
}
// 解決“雙黑”問題:整個操作只需常數次旋轉操作即可完成調整
// parent 爲 node 之父,之所以需要傳遞 parent,是爲了解決 node 爲 null 的情況
private void solveDoubleBlack(RedBlackNode<T> node, RedBlackNode<T> parent) {
// node 爲根節點,無需操作
if (parent == null) {
return;
}
// node 的兄弟
RedBlackNode<T> sibling = (node == parent.left) ? parent.right : parent.left;
// sibling 爲黑色
if (isBlack(sibling)) {
// sibling 的紅色孩子,若左、右孩子皆紅,左者優先;都爲黑時爲 null
RedBlackNode<T> redChild = null;
if (sibling.left != null && isRed(sibling.left)) {
redChild = sibling.left;
} else if (sibling.right != null && isRed(sibling.right)) {
redChild = sibling.right;
}
// 有紅色孩子:情況 1
if (redChild != null) {
int color = parent.color;
RedBlackNode<T> grandparent = parent.parent; // 曾祖父節點
RedBlackNode<T> rootOfSubtree; // 重平衡之後,子樹的根
if (grandparent == null) { // 重平衡之後,子樹的根即是整棵樹的樹根
rootOfSubtree = root = rotateAt(redChild );
} else { // 否則,將重平衡之後的子樹接入,成爲原曾祖父的孩子
if (isLeftChild(grandparent)) {
rootOfSubtree = grandparent.left = rotateAt(redChild );
} else {
rootOfSubtree = grandparent.right = rotateAt(redChild );
}
}
// 將左右孩子染黑
rootOfSubtree.left.color = BLACK;
rootOfSubtree.right.color = BLACK;
// 繼承原 p 節點的顏色
rootOfSubtree.color = color;
rootOfSubtree.parent = grandparent;
}
// 無紅色孩子
else {
sibling.color = RED; // 將 silibing 染紅(情況2、3中,s 均被染紅)
// 父節點爲紅色:情況 2
if (isRed(parent)) {
parent.color = BLACK;
}
// 父節點爲黑色:情況 3
else {
solveDoubleBlack(parent, parent.parent);
}
}
}
// sibling 爲紅色:情況 4
else {
// sibling 轉黑, parent 轉紅
sibling.color = BLACK;
parent.color = RED;
// 取 s 同側的孩子
RedBlackNode<T> child = isLeftChild(sibling) ? sibling.left : sibling.right;
RedBlackNode<T> grandparent = parent.parent;
RedBlackNode<T> rootOfSubtree;
// 旋轉:將情況 4 轉爲 情況 1/情況2
if (grandparent == null) {
rootOfSubtree = root = rotateAt(child);
} else {
if (isLeftChild(grandparent)) {
rootOfSubtree = grandparent.left = rotateAt(child);
} else {
rootOfSubtree = grandparent.right = rotateAt(child);
}
}
rootOfSubtree.parent = grandparent;
// 遞歸解決:此時已是情況1/情況2
solveDoubleBlack(node, parent);
}
}
重平衡操作
具體解釋見:AVL 樹 - Java 實現 - 重平衡操作
// “3+4”重構:重構子樹
private RedBlackNode<T> connect34(RedBlackNode<T> T0, RedBlackNode<T> a, RedBlackNode<T> T1, RedBlackNode<T> b,
RedBlackNode<T> T2, RedBlackNode<T> c, RedBlackNode<T> T3) {
a.left = T0;
a.right = T1;
if (T0 != null) {
T0.parent = a;
}
if (T1 != null) {
T1.parent = a;
}
c.left = T2;
c.right = T3;
if (T2 != null) {
T2.parent = c;
}
if (T3 != null) {
T3.parent = c;
}
b.left = a;
b.right = c;
a.parent = c.parent = b;
return b; // 新子樹的根
}
// 視具體情況完成重平衡操作
// 返回重構之後子樹的根
private RedBlackNode<T> rotateAt(RedBlackNode<T> node) {
RedBlackNode<T> parent = node.parent;
RedBlackNode<T> grandparent = parent.parent;
if (isLeftChild(parent)) { // 左
if (isLeftChild(node)) { // 左
parent.parent = grandparent.parent;
return connect34(node, parent, grandparent, node.left, node.right, parent.right, grandparent.right);
} else { // 右
node.parent = grandparent.parent;
return connect34(parent, node, grandparent, parent.left, node.left, node.right, grandparent.right);
}
} else { // 右
if (isRightChild(node)) { // 右
parent.parent = grandparent.parent;
return connect34(grandparent, parent, node, grandparent.left, parent.left, node.left, node.right);
} else { // 左
node.parent = grandparent.parent;
return connect34(grandparent, node, parent, grandparent.left, node.left, node.right, parent.right);
}
}
}
與 AVL 樹的差別
對紅黑樹來說,在節點插入和節點刪除後的重平衡過程中,一旦完成了旋轉操作,整個重平衡過程必然隨即完成(需要遞歸重平衡的情況下,不涉及旋轉操作)。也就是說,紅黑樹的插入、刪除操作僅涉及常數次拓撲結構的調整操作!這是紅黑樹與AVL樹的一項本質差別。
在 AVL 樹的插入操作中,也只需常數次的調整操作,但在其刪除操作中,可能需要多次調整操作。