【資料結構與演算法】之紅黑樹 --- 第十四篇
樹是一種非線性資料結構,這種資料結構要比線性資料結構複雜的多,因此分為三篇部落格進行講解:
第三篇:紅黑樹
第三篇:紅黑樹
開篇說明:對於紅黑樹的學習,近階段只需要掌握這種資料結構的思想、特點、適用場景以及它能夠解決的問題即可,它的實現過程比較複雜,一般面試中也不會讓你手寫紅黑樹程式碼,而且實際程式碼開發中,幾乎也不會遇到手寫紅黑樹的場景。以後,學有餘力的時候,再對紅黑樹進行深入的瞭解。
1、紅黑樹的基本概念
在上一篇文章中,講述了二叉查詢樹,它支援快速插入、刪除和查詢操作,並且各個操作的時間複雜度跟樹的高度成正比,理想情況下,時間複雜度是O(logn)。但是,二叉查詢樹在頻繁的動態更新過程中,可能會出現樹的高度遠遠大於
1.1 紅黑樹的定義
首先我們需要先了解下“平衡二叉查詢樹”的概念:二叉樹中任意一個結點的左右子樹高度相差不能大於1。所以完全二叉樹和滿二叉樹都是平衡二叉查詢樹,但是非完全二叉樹也有可能是平衡二叉查詢樹。常見的平衡二叉查詢樹有:AVL樹、Sply Tree(伸展樹)、Treap(樹堆)等。
雖然我們平時提到最多的平衡二叉查詢樹是紅黑樹,但是實際上紅黑樹並不是嚴格意義上的平衡二叉查詢樹
所以,平衡二叉查詢樹中“平衡”的意思其實就是讓整棵樹左右看起來比較“對稱”,比較“平衡”,不要出現左子樹很高而右子樹很矮的情況。這樣就能讓整棵樹的高度相對來說低一些,相應的插入、刪除和查詢等操作的效率高一些。
因此,如果我們設計一個新的平衡二叉查詢樹,只要它的高度不比大很多,比如樹的高度仍然還是對數級的,它仍然是一棵合格的平衡二叉查詢樹。
下面我們就正式來講解下紅黑樹的概念和它的特性:
紅黑樹R-B Tree,全稱是:Red-Black Tree,它是一種特殊的二叉查詢樹。紅黑樹的每個結點要麼被標記為紅色,要麼被標記為黑色。同時一棵紅黑樹還需要滿足以下4點要求:
- 根節點是黑色的;
- 每個葉子結點都是黑色的空結點,即葉子結點不儲存資料(這樣做是為了便於程式碼實現);
- 任何相鄰的結點都不能同時為紅色,即紅色結點是被黑色結點隔開的;
- 每個結點,從該結點到達其可達葉子結點的所有路徑,都包含相同數目的黑色結點。
- 上面的這些約束條件體現了紅黑樹的一個關鍵性質:從根結點到葉子結點最長的可能路徑不多於最短的可能路徑的兩倍長。
1.2 紅黑樹的時間複雜度
一棵含有n個結點的紅黑樹的高度至多為2log(n+1)。具體數學證明過程點選:證明過程。
上面連結裡的文章使用了數學歸納法對紅黑樹的時間複雜度進行了證明,本文采納了《資料結構與演算法之美》專欄裡的簡單證明方法,旨在說明問題,不追求精確的計算結果。
二叉查詢樹的很多操作的時間複雜度都是和樹的高度成正比的,一棵及其平衡的二叉樹(滿二叉樹或者完全二叉樹)的高度大約是,所以如果要證明紅黑樹是近似平衡的,只需要分析它的高度穩定在附近即可。
首先,我們將紅色結點從紅黑樹中去掉,那麼有些結點就沒有父結點了,它們會直接拿這些結點的祖父結點作為父結點。所以,之前的二叉樹就變成了四叉樹,再從四叉樹中取出某些結點放到葉子結點位置,四叉樹就變成了完全二叉樹。所以,僅包含黑色結點的四叉樹高度比包含相同結點個數的完全二叉樹的高度()還要小,所以去掉紅色結點的“黑樹”的高度也不會超過。
通過上面的講解,我們知道去掉紅色結點的“黑樹”的高度也不會超過。然而,在紅黑樹中,紅色結點是被黑色結點隔開而不能相鄰的。也就是說,有一個紅色結點至少要有一個黑色結點,將它與其他紅色結點隔開。紅黑樹中包含最多黑色結點的路徑不會超過(因為去掉紅色結點的“黑樹”的高度不會超過)。所以加入紅色結點後,最長路徑也不會超過2,即紅黑樹的高度近似於2,故其常用操作的時間複雜度都穩定在O(logn)。
說明:雖然上面的推到過程不夠嚴謹,但已經能夠說明問題,雖然紅黑樹的高度比嚴格意義上的平衡二叉樹(AVL)大了一倍,但是效能下降的並不多。紅黑樹只做到了近似平衡,並不是嚴格意義上的平衡,所以在維護成本上要比AVL樹低。
1.3 紅黑樹的Java程式碼實現
public class RB_Tree {
public static final boolean RED = true;
public static final boolean BLACK = false;
public Node root; // 根結點
// 結點內部類
class Node{
public int data; // 值
public Node left, right; // 左子樹和右子樹
public int N; // 以該結點為根結點的子樹中的結點總數
public boolean color; // 結點顏色
public Node(int data, int n, boolean color) {
super();
this.data = data;
N = n;
this.color = color;
}
}
// 獲取整個紅黑樹的大小
public int size(){
return size(root);
}
// 獲取以某一結點為根結點的樹的大小
public int size(Node node){
if(node == null){
return 0;
}else{
return node.N;
}
}
// 判斷當前結點是否為紅色結點
public boolean isRed(Node node){
if(node == null){
return false;
}
return node.color == RED;
}
}
2、紅黑樹的基本操作(一)左旋、右旋、顏色轉換
紅黑樹在插入和刪除過程中可能會破壞原本的平衡條件導致不滿足紅黑樹的性質,這時候一般需要通過左旋、右旋和重新著色這三個操作來使紅黑樹重新滿足平衡化條件。
2.1 左旋
通常左旋操作用於將一個向右傾斜的紅色連結(這個紅色連結連線的兩個結點均是紅色結點)旋轉為左連線。對比前後操作,可以看出,該操作實際上是將紅連結的兩個結點中較大的結點移動到根節點上。
左旋的動畫效果如下: 【動畫圖來自:https://blog.csdn.net/u010853261/article/details/54312932】
// 左旋轉
public Node rotateLeft(Node h){
Node x = h.right;
// 把x的左結點賦值給h的右結點
h.right = x.left;
// 把h賦值給x的左結點
x.left = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(h.left) + size(h.right) + 1;
return x;
}
2.2 右旋
右旋其實是左旋的逆操作,如下所示:
// 右旋轉
public Node rotateRight(Node h){
Node x = h.left;
// 把x的右結點賦值給h的左結點
h.left = x.right;
// 將h賦值給x的右結點
x.right = h;
x.color = h.color;
h.color = RED;
x.N = h.N;
h.N = size(h.left) + size(h.right) + 1;
return x;
}
右旋的動態示意圖:
2.3 顏色反轉
當出現一個臨時4-node的時候,即一個結點的兩個子結點均為紅色,如下圖A所示。我們需要將E提升至父結點,即把E對子結點的連線設定為黑色,自己的顏色設定為紅色。顏色反轉之後如圖B所示。
// 顏色反轉
public void flipColors(Node h){
h.color = RED; // 父結點的顏色變紅
h.left.color = BLACK; // 左子結點顏色變黑
h.right.color = BLACK; // 右子結點顏色變黑
}
3、紅黑樹的基本操作(二)插入、刪除 ---- 瞭解思想
具體操作分析可以參考:
3.1 插入操作
紅黑樹規定,插入的結點必須是紅色的。而且,二叉查詢樹新插入的結點都是放在葉子結點上,所以關於插入操作的平衡調整,有下面這兩種特殊情況:
1、如果插入結點的父結點是黑色,那我們什麼都不用做,它仍滿滿足紅黑樹的定義;
2、如果插入的結點是根結點,那我們只需要改變它的顏色,把它變成黑色就行了。
除了上訴的兩種特殊情況外,其他情況都會違背紅黑樹的定義,需要通過左右選擇和顏色轉換進行調整。
紅黑樹的平衡調整過程是一個迭代的過程,我們把正在處理的結點叫做關注結點。關注結點會隨著不停地迭代處理,而不斷地發生變化。最開始的關注結點就是新插入的結點。
新結點插入後,如果紅黑樹的平衡被打破,一般會有三種情況。我們只需要根據每種情況的特點,不停地調整,就可以讓紅黑樹繼續符合定義,繼續保持平衡。
現象說明 | 處理策略 | |
---|---|---|
Case 1 | 當前節點的父節點是紅色,且當前節點的祖父節點的另一個子節點(叔叔節點)也是紅色。 | (01) 將“父節點”設為黑色。 |
Case 2 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的右孩子 | (01) 將“父節點”作為“新的當前節點”。 |
Case 3 | 當前節點的父節點是紅色,叔叔節點是黑色,且當前節點是其父節點的左孩子 | (01) 將“父節點”設為“黑色”。 |
說明:上面三種情況處理問題的核心思想都是:將紅色結點移到根結點,然後,再將根結點設定為黑色。
3.2 刪除操作
刪除操作的平衡調整分為兩步,第一步是針對刪除結點的初步調整,初步調整隻保證整棵紅黑樹在一個結點刪除之後,仍然滿足最後一條要求(每個結點,從該結點到達其可達葉子結點的所有路徑,都包含相同數目的黑色結點);第二步是針對關注結點進行二次調整,讓它滿足紅黑樹的第三條要求(不存在相鄰的兩個紅色結點)。
刪除操作有以下四種情況:
現象說明 | 處理策略 | |
---|---|---|
Case 1 | x是"黑+黑"節點,x的兄弟節點是紅色。(此時x的父節點和x的兄弟節點的子節點都是黑節點)。 | (01) 將x的兄弟節點設為“黑色”。 |
Case 2 | x是“黑+黑”節點,x的兄弟節點是黑色,x的兄弟節點的兩個孩子都是黑色。 | (01) 將x的兄弟節點設為“紅色”。 |
Case 3 | x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的左孩子是紅色,右孩子是黑色的。 | (01) 將x兄弟節點的左孩子設為“黑色”。 |
Case 4 | x是“黑+黑”節點,x的兄弟節點是黑色;x的兄弟節點的右孩子是紅色的,x的兄弟節點的左孩子任意顏色。 | (01) 將x父節點顏色 賦值給 x的兄弟節點。 |
4 總結:
紅黑樹是一種平衡二叉查詢樹(當然不是嚴格意義上的平衡二叉查詢樹),它是為了解決普通二叉查詢樹在資料更新的過程中複雜度退化問題。紅黑樹的高度近似於,所以它是近似平衡,其插入、刪除和查詢操作的時間複雜度都是O(logn)。
也正是因為紅黑樹是一種效能非常穩定的二叉查詢樹,所以,在工程中,但凡用到動態插入、刪除和查詢資料的場景,都可以用到它。但是它的程式碼實現起來比較困難。所以重點在於學習它的思想和能夠解決的問題。
對於紅黑樹的插入和刪除操作,目前基礎比較薄弱,選擇暫時不深究。
參考及推薦:
1、《資料結構與演算法之美》:https://time.geekbang.org/column/article/68638
2、紅黑樹(一)之 原理和演算法詳細介紹:http://www.cnblogs.com/skywang12345/p/3245399.html
3、紅黑樹(五)之 Java的實現:https://www.cnblogs.com/skywang12345/p/3624343.html