前言
理解紅黑樹需要掌握下面知識
- 二分查詢演算法
- 二叉查詢樹
- 自平衡樹(AVL樹和紅黑樹)
基於二分演算法設計出了二叉查詢樹,為了彌補二叉查詢樹傾斜缺點,又出現了一些自平衡樹,比如AVL樹,紅黑樹等。
二分查詢演算法
在40億資料中查詢一個指定資料最多隻需要32次,這就是二分查詢演算法的魅力。
二分查詢演算法(又叫折半查詢演算法)是一種在有序陣列中查詢某一特定元素的搜尋演算法。注意有序陣列的前提。
下圖中查詢 4 ,查詢從中間元素開始 4 < 7
,從左邊查詢 4 > 3
,從右邊查詢 4 < 6
,然後找到元素。
二分查詢演算法時間和空間複雜度,\( {n} \) 是陣列長度。
平均時間複雜度 \( {O(\log n)} \)
最壞時間複雜度 \( {O(\log n)} \)
最優時間複雜度 \( {O(1)} \)
迴圈空間複雜度 \( {O(1)} \)
遞迴空間複雜度 \( {O(\log n)} \)
Java 遞迴實現二分查詢。
public static int binarySearch(int[] arr, int start, int end, int hkey) {
if (start > end) {
return -1;
}
int mid = start + (end - start) / 2; //防止溢位
if (arr[mid] > hkey) {
return binarySearch(arr, start, mid - 1, hkey);
}
if (arr[mid] < hkey) {
return binarySearch(arr, mid + 1, end, hkey);
}
return mid;
}
Java 迴圈實現二分查詢。
public static int binarySearch(int[] arr, int start, int end, int hkey) {
int result = -1;
while (start <= end) {
int mid = start + (end - start) / 2; //防止溢位
if (arr[mid] > hkey) {
end = mid - 1;
} else if (arr[mid] < hkey) {
start = mid + 1;
} else {
result = mid;
break;
}
}
return result;
}
二叉查詢樹
二叉查詢樹(Binary Search Tree,簡稱BST)是一棵二叉樹,它具有以下性質:
- 若任意節點的左子樹不空,則左子樹上所有節點的值都小於它的根節點的值;
- 若任意節點的右子樹不空,則右子樹上所有節點的值都大於它的根節點的值;
- 任意節點的左、右子樹也分別為二叉查詢樹。
二叉樹:每個節點最多隻有兩個分支,分別稱為“左子樹”或“右子樹”。
二叉查詢樹操作(搜尋,插入,刪除)效率依賴樹高度。
最壞情況,樹向一邊傾斜,樹高度 $n$ (節點數量),此時操作時間複雜度為 $O(n)$
理想情況,樹高度 $log(n)$ ,操作時間複雜度 $O(log(n))$ ,此時它是一棵平衡的二叉查詢樹。
演算法 | 平均 | 最差 |
---|---|---|
空間 | O(n) | O(n) |
搜尋 | O(log n) | O(n) |
插入 | O(log n) | O(n) |
刪除 | O(log n) | O(n) |
為了讓二叉查詢樹儘可能達到理想情況,出現了一些自平衡二叉查詢樹,如AVL樹和紅黑樹。
AVL樹
AVL樹中的每個節點都有一個平衡因子屬性(左子樹高度減去右子樹高度)。每次元素插入刪除操作後,會重新進行平衡計算,如果節點平衡因子不為 [1,0,-1] 時,需要通過旋轉使樹到達平衡。AVL 樹中有 4 種旋轉操作。
- 左旋(Left Rotation)
- 右旋(RightRotation)
- 左右旋轉(Left-Right Rotation)
- 左右旋轉(Right-Left Rotation)
下面是 Java AVL 樹的例子
private Node insert(Node node, int key) {
.....
return rebalance(node); // 重新平衡計算
}
private Node delete(Node node, int key) {
.....
node = rebalance(node); // 重新平衡計算
return node;
}
private Node rebalance(Node z) {
updateHeight(z);
int balance = getBalance(z);
if (balance > 1) {
if (height(z.right.right) > height(z.right.left)) {
z = rotateLeft(z);
} else {
z.right = rotateRight(z.right);
z = rotateLeft(z);
}
} else if (balance < -1) {
if (height(z.left.left) > height(z.left.right)) {
z = rotateRight(z);
} else {
z.left = rotateLeft(z.left);
z = rotateRight(z);
}
}
return z;
}
https://github.com/eugenp/tut...
紅黑樹
性質
紅黑樹中的每個節點都有一個顏色屬性。每次元素插入刪除操作後,會進行重新著色和旋轉達到平衡。
紅黑樹屬於二叉查詢樹,它包含二叉查詢樹性質,同時還包含以下性質:
- 每個節點要麼是黑色,要麼是紅色。
- 所有的葉子節點(NIL)被認為是黑色的。
- 每個紅色節點的兩個子節點一定都是黑色(不會出現兩個連續紅色節點)。
- 從根到葉子節點(NIL)的每條路徑都包含相同數量的黑色節點。
查詢
查詢不會破壞樹的平衡,邏輯也比較簡單,通常有以下幾個步驟。
- 從根節點開始查詢,把根節點設定為當前節點;
- 當前節點為空,返回null;
- 當前節點不為空,查詢key小於當前節點key,左子節點設為當前節點。
- 當前節點不為空,查詢key大於當前節點key,右子節點設為當前節點。
- 當前節點不為空,查詢key等於當前節點key,返回當前節點。
程式碼實現可以參考 Java 裡面的 TreeMap。
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0){
p = p.left;
}else if (cmp > 0){
p = p.right;
}else{
return p;
}
}
return null;
插入
插入操作分兩大塊:一查詢插入位置;二插入後自平衡。
- 將根節點賦給當前節點,迴圈查詢插入位置的節點;
- 當查詢key等於當前節點key,更新節點儲存的值,返回;
- 當查詢key小於當前節點key,把當前節點的左子節點設定為當前節點;
- 當查詢key大於當前節點key,把當前節點的右子節點設定為當前節點;
- 迴圈結束後,構造新節點作為當前節點左(右)子節點;
- 通過旋轉變色進行自平衡。
程式碼實現可以參考 Java 裡面的 TreeMap。
Entry<K,V> t = root;
Entry<K,V> parent;
int cmp;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0){
t = t.left;
}else if (cmp > 0){
t = t.right;
}else {
return t.setValue(value); // 更新節點的值,返回
}
} while (t != null);
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0){
parent.left = e;
}else {
parent.right = e;
}
fixAfterInsertion(e); // 通過旋轉變色自平衡
插入場景分析
- 根節點為空,將插入節點設定為根節點並設定為黑色;
- 插入節點的key已存在,只需要更新插入值,無需再自平衡;
- 插入節點的父節點為黑色,直接插入,無需自平衡;
- 插入節點的父節點為紅色。
場景 4 插入節點後出現兩個連續的紅色節點,所以需要重新著色和旋轉。這裡面又有很多種情況,具體看下面。
先宣告下節點關係,祖節點(10),叔節點(20),父節點(9),插入節點(8)。
一般通過判斷插入節點的叔節點來確定合適的平衡操作。
叔叔節點存在且為紅色。
- 先查詢位置將節點8 插入;
- 將父節點9 和叔節點20 變為黑色,祖節點10 變為紅色;
- 祖節點10 是根節點,所以又變為黑色。
叔叔節點不存在或為黑色,父節點是祖節點的左節點,插入節點是父節點的左子節點。
- 先查詢位置將節點7 插入;
- 將祖節點9 進行右旋轉;
- 將父節點8 變為黑色,祖節點9 變為紅色;
叔叔節點不存在或為黑色,父節點是祖節點的左節點,插入節點是父節點的右子節點。
- 先查詢位置將節點8 插入;
- 將父節點7 進行左旋轉;
- 將祖節點9 進行右旋轉;
- 將插入節點8 變為黑色,祖節點9 變為紅色;
叔叔節點不存在或為黑色,父節點是祖節點的右節點,插入節點是父節點的右子節點。
- 先查詢位置將節點10 插入;
- 將祖節點8 進行左旋轉;
- 將父節點9 變為黑色,祖節點8 變為紅色;
叔叔節點不存在或為黑色,父節點是祖節點的右節點,插入節點是父節點的左子節點。
- 先查詢位置將節點9 插入;
- 將父節點10 進行右旋轉;
- 將祖節點8 進行左旋轉;
- 將插入節點9 變為黑色,祖節點8 變為紅色;
刪除
刪除操作分兩大塊:一查詢節點刪除;二刪除後自平衡。刪除節點後需要找節點來替代刪除的位置。
根據二叉查詢樹性質,刪除節點之後,可以用左子樹中的最大值或右子樹中的最小值來替換刪除節點。如果刪除的節點無子節點,可以直接刪除,無需替換;如果只有一個子節點,就用這個子節點替換。
思考一些刪除場景,使用下面視覺化工具模擬場景。
https://www.cs.csubak.edu/~ms...
替換節點和刪除節點其中一個紅色
- 查詢到刪除節點3,將它刪除;
- 節點2 替換刪除位置,並變為刪除節點3 的黑色。
替換節點和刪除節點都是黑色,它兄弟節點是黑色,兄弟節點的子節點至少有一個紅色。
替換節點和刪除節點都是黑色,它兄弟節點是黑色,兄弟節點的子節點至少有一個紅色。
替換節點和刪除節點都是黑色,它兄弟節點是黑色,兄弟節點的兩個子節點都是黑色。
替換節點和刪除節點都是黑色,它兄弟節點是紅色。
AVL樹和紅黑樹對比
下面是[1-10]分別儲存在AVL樹和紅黑樹的圖片。可以看出:
- AVL樹更嚴格平衡,帶來查詢速度快。為了維護嚴格的平衡,需要付出頻繁旋轉的效能代價。
- 紅黑樹相較於要求嚴格的AVL樹來說,它的旋轉次數少。