資料結構——平衡樹之紅黑樹
一、2-3樹
在瞭解什麼是紅黑樹之前,首先需要補充一下什麼是2-3樹,因為它有助於我們對紅黑樹的理解,包括對B類樹的理解。
2-3樹可以有兩個孩子或三個孩子,所以也就被稱為2-3樹,且2-3滿足二分搜尋樹的基本性質。如左圖中,a的左孩子值 < a,a右孩子的值 > a,在右圖中可以存放兩個元素b、c,該樹有三個孩子,左孩子的值 < b,b < 中間孩子的值 < c,右孩子的值 > c。通常稱左圖含有兩個孩子的節點為2節點,稱右圖中含有三個孩子的節點為3節點。
當我們需要對2-3進行搜尋的時候,實際上也是和二分搜尋樹思路一致的,當我們搜尋的過程來到一個3節點的話,搜尋數x如果小於3節點的左值,則在3節點的左子樹中繼續尋找,如果搜尋的這個樹大於3節點的右值,到三節點的右子樹中繼續查詢,如果搜尋數x的值介於3節點的左值與右值之間,則在3節點的中間子樹繼續尋找。
對於2-3樹來說,它是一個絕對平衡的樹。即從根節點到任意一個葉子節點,所經過的節點數量一定是相同的。
理解在2-3樹中新增節點時,是如何維護絕對平衡的,對我們理解紅黑樹的機制很有幫助。
2-3樹在新增節點時,如果子樹為空,則進行融合,如果融合之後是四節點,則分裂。
如圖:
在新增元素6時,元素12不存在左子樹,則元素12與元素6融合形成3節點
融合形成4節點則分裂
插入後形成3節點,且父親節點為2節點時
插入後形成3節點,且父親節點為3節點時
二、2-3樹與紅黑樹的等價性
黑色為普通節點,紅色為特殊節點,紅黑組合相當於2-3樹中的3節點,我們把3節點中的左節點b作為右節點c的 左子節點 ,因為左節點 < 右節點 ,之所以b為紅色是因為在2-3樹中,元素b元素c是作為3節點相融在一起的,但在紅黑樹中只有2節點,紅色表示【紅色的b】與其父節點【黑色的c】在2-3樹是以3節點相融的,在紅黑樹中,所有的紅色節點都是向左傾斜的。
轉化為對應的紅黑樹:
三、紅黑樹的五大特性
1、每個節點或者是紅色的或者是黑色的。
2、根節點是黑色的。
3、每一個葉子節點(最後的空節點NIL)是黑色的。
4、如果一個節點是紅色的,那麼他的孩子節點一定是黑色的。
5、從任意一個節點到葉子節點,所經過的黑色節點數量相同。
性質2證明: 根節點是黑色的
性質3是一種性質:每一個葉子節點(最後的空節點NIL)是黑色的
對於一棵空樹本身,其根節點一定是黑色的,在極端的環境下,空樹即是葉節點又是根節點,都是黑色的
性質4證明:如果一個節點是紅色的,那麼他的孩子節點一定是黑色的
在紅黑樹中,紅黑節點組合代表的是2-3樹中的3節點,3節點中左側元素b對應紅黑樹中的紅色節點,該紅色節點的孩子為對應2-3樹中的左孩子和中間孩子,若該孩子節點為2節點,如左邊圖所示,則一定為黑色;若孩子節點為3節點,則連線的形狀如右圖所示,先連線黑色節點,黑色節點的左孩子才為空色節點,所以如果一個節點它是紅色的,它的孩子節點一定是黑色的。
拓展:對於一個黑色節點,他的左孩子可能為紅色【對應2-3樹的融合】,也可能為黑色。 總的來說:它的左孩子右可能為紅色,有可能為黑色,但右節點一定是黑色節點。
性質5【紅黑樹核心特性】證明:從任意一個節點到葉子節點,所經過的黑色節點數量相同
因為紅黑樹和2-3樹是等價的,2-3樹是一棵絕對平衡的樹。對於2-3樹的任意一個節點出發,到葉子節點所經過的節點數一樣多,由於2-3樹是絕對平衡的樹,所有的葉子節點都在同一層中。
在2-3樹轉化為紅黑樹時,對應2節點/3節點分別轉化為黑色節點/紅色和黑色節點2個節點,不管如何轉化一定會存在一個黑色節點,所以從任意一個節點到葉子節點,對於2-3樹來說經過的節點數相同,對紅黑樹來說經過的黑色節點數量相同。
紅黑樹是保持"黑平衡"的二叉樹,嚴格意義上講紅黑樹並不是平衡二叉樹,左右子樹的黑色節點保持著絕對的平衡,對紅黑樹來說,如果存在節點個數n,那麼最大高度為2logn,時間複雜度為O(logn)。
AVL vs 紅黑樹
查詢效率: AVL樹 > 紅黑樹
增刪改效率: AVL樹 < 紅黑樹
四、向紅黑樹中新增新元素
由於2-3樹和紅黑樹具有等價性,先回憶在2-3樹中新增新節點時,永遠不會新增到一個空節點【要麼融合,要麼產生臨時4節點分裂】,所以新增進2節點時,會形成一個3節點,當新增進3節點時,會暫時形成一個4節點後分裂。
在紅黑樹中新增新元素時,設定為永遠是紅色節點,在遞迴結束後,我們需要手動設定根節點為黑色BLACK(boolean)。
新增元素時,有幾種情況:
新增到對應2-3樹中的2節點下:
1、新增元素新增到左右子樹都為空的黑色節點[2-node]
①新插入節點比其根節點小,則作為根節點的左子節點,這種情況比較簡單【相當於2-3樹融合】
②新插入節點比根節點大,則作為根節點的右子節點,我們知道紅色節點不可能在右側,所以需要做調整
執行左旋轉
步驟1:
步驟2:
為了表示37、42是一個3節點,則原來的元素37需要變為紅色節點
其中第三句x.color = node.color,如果原來node顏色為紅色節點,則打破了紅黑樹的基本性質,在這裡左旋轉只是一個子過程,左旋轉後形成子樹新的根節點x將會被返回做後續處理,在左旋轉時並不維持紅黑樹的基本性質,只需要保證37、42兩個元素對應2-3樹中的三節點即可。
左旋轉程式碼:
// node x
// / \ 左旋轉 / \
// T1 x ---------> node T3
// / \ / \
// T2 T3 T1 T2
private Node leftRotate(Node node){
Node x = node.right;
node.right = x.left;
x.left = node;
//切換顏色
x.color = node.color;
node.color = RED;
//返回旋轉之後的根節點
return x;
}
2、新增元素新增到黑色節點[3-node]
①向原3節點的紅黑樹中新增一個大於其根節點42的值
步驟1:根據二分搜尋樹的新增規則,66新增到37的右節點上
對應的是一個臨時的4節點【融合】 處理方式為:
步驟2:拆分成3個2節點
對應紅黑樹為3個黑色節點,讓42的左右子節點都改變為黑色
步驟3:
此時其根節點42應該與42的父節點進行融合,所以42需要變為紅色
我們發現42由黑色變為了紅色,而37和66都由紅色變為了黑色,該動作稱為顏色翻轉【flipColors】
//顏色翻轉
private void flipColors(Node node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
②向原3節點的紅黑樹中新增一個小於其父節點的值
新增後:
此時產生了一個臨時的4節點,處理方式仍然為分裂,即變為由3個2節點的子樹,如圖:
此時應該執行右旋轉,步驟1:
旋轉之前:
旋轉之後:
步驟2:此時需要變換顏色,x的顏色需要改變為node的顏色,node的顏色需要設定為紅色
最後執行我們編寫過的顏色翻轉即可,變為:
右旋轉程式碼:
// node x
// / \ 右旋轉 / \
// x T2 -------> y node
// / \ / \
// y T1 T1 T2
public Node rightNode(Node node){
Node x = node.left;
node.left = x.right;
x.right = node;
//切換顏色
x.color = node.color;
node.color = RED;
return x;
}
③向原3節點的紅黑樹中新增一個大於根節點且大於其父節點的值
新增後形成的紅黑樹情況:
步驟1:對節點37進行左旋轉,如圖
步驟2:對節點42進行右旋轉,如圖:
步驟3:變色操作,對節點40變為42的顏色,將節點42變為紅色
步驟4:執行顏色翻轉
新增元素總結
這種過程即為我上述的向紅黑樹3節點中新增元素【新增的元素小於根節點但大於新增位置的父節點】的全過程
向紅黑樹3節點中新增元素【新增的元素小於根節點且小於新增位置的父節點】的全過程,即直接跳到了第三步
向紅黑樹3節點中新增元素【新增的元素大於根節點】的全過程,即直接跳到了第四步
左右旋轉條件總體邏輯程式碼編寫:
左旋轉 右節點為紅色且左節點不為紅色
右旋轉 左節點為紅色且左節點的左節點也為紅色
顏色翻轉
在add方法中:【使用3個if 每次都順序判斷】
// 向以node為根的二分搜尋樹中插入元素(key, value),遞迴演算法
// 返回插入新節點後二分搜尋樹的根
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value); //預設插入紅色節點
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else // key.compareTo(node.key) == 0
node.value = value;
//左旋轉 右節點為紅色且左節點不為紅色
if(isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
//右旋轉 左節點為紅色且左節點的左節點也為紅色
if(isRed(node.left) && isRed(node.left.left))
node = rightNode(node);
//顏色翻轉
if(isRed(node.left) && isRed(node.right))
flipColors(node);
return node;
}
紅黑樹的效能總結:
紅黑樹完整程式碼:
import java.util.ArrayList;
public class RBTree<K extends Comparable<K>, V> {
private static final boolean RED = true;
private static final boolean BLACK = false;
private class Node{
public K key;
public V value;
public Node left, right;
public boolean color;
public Node(K key, V value){
this.key = key;
this.value = value;
left = null;
right = null;
color = RED;
}
}
private Node root;
private int size;
public RBTree(){
root = null;
size = 0;
}
public int getSize(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
// 判斷節點node的顏色
private boolean isRed(Node node){
if(node == null)
return BLACK;
return node.color;
}
// node x
// / \ 左旋轉 / \
// T1 x ---------> node T3
// / \ / \
// T2 T3 T1 T2
private Node leftRotate(Node node){
Node x = node.right;
// 左旋轉
node.right = x.left;
x.left = node;
x.color = node.color;
node.color = RED;
return x;
}
// node x
// / \ 右旋轉 / \
// x T2 -------> y node
// / \ / \
// y T1 T1 T2
private Node rightRotate(Node node){
Node x = node.left;
// 右旋轉
node.left = x.right;
x.right = node;
x.color = node.color;
node.color = RED;
return x;
}
// 顏色翻轉
private void flipColors(Node node){
node.color = RED;
node.left.color = BLACK;
node.right.color = BLACK;
}
// 向紅黑樹中新增新的元素(key, value)
public void add(K key, V value){
root = add(root, key, value);
root.color = BLACK; // 最終根節點為黑色節點
}
// 向以node為根的紅黑樹中插入元素(key, value),遞迴演算法
// 返回插入新節點後紅黑樹的根
private Node add(Node node, K key, V value){
if(node == null){
size ++;
return new Node(key, value); // 預設插入紅色節點
}
if(key.compareTo(node.key) < 0)
node.left = add(node.left, key, value);
else if(key.compareTo(node.key) > 0)
node.right = add(node.right, key, value);
else // key.compareTo(node.key) == 0
node.value = value;
if (isRed(node.right) && !isRed(node.left))
node = leftRotate(node);
if (isRed(node.left) && isRed(node.left.left))
node = rightRotate(node);
if (isRed(node.left) && isRed(node.right))
flipColors(node);
return node;
}
// 返回以node為根節點的二分搜尋樹中,key所在的節點
private Node getNode(Node node, K key){
if(node == null)
return null;
if(key.equals(node.key))
return node;
else if(key.compareTo(node.key) < 0)
return getNode(node.left, key);
else // if(key.compareTo(node.key) > 0)
return getNode(node.right, key);
}
public boolean contains(K key){
return getNode(root, key) != null;
}
public V get(K key){
Node node = getNode(root, key);
return node == null ? null : node.value;
}
public void set(K key, V newValue){
Node node = getNode(root, key);
if(node == null)
throw new IllegalArgumentException(key + " doesn't exist!");
node.value = newValue;
}
// 返回以node為根的二分搜尋樹的最小值所在的節點
private Node minimum(Node node){
if(node.left == null)
return node;
return minimum(node.left);
}
// 刪除掉以node為根的二分搜尋樹中的最小節點
// 返回刪除節點後新的二分搜尋樹的根
private Node removeMin(Node node){
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
// 從二分搜尋樹中刪除鍵為key的節點
public V remove(K key){
Node node = getNode(root, key);
if(node != null){
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key){
if( node == null )
return null;
if( key.compareTo(node.key) < 0 ){
node.left = remove(node.left , key);
return node;
}
else if(key.compareTo(node.key) > 0 ){
node.right = remove(node.right, key);
return node;
}
else{ // key.compareTo(node.key) == 0
// 待刪除節點左子樹為空的情況
if(node.left == null){
Node rightNode = node.right;
node.right = null;
size --;
return rightNode;
}
// 待刪除節點右子樹為空的情況
if(node.right == null){
Node leftNode = node.left;
node.left = null;
size --;
return leftNode;
}
// 待刪除節點左右子樹均不為空的情況
// 找到比待刪除節點大的最小節點, 即待刪除節點右子樹的最小節點
// 用這個節點頂替待刪除節點的位置
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
}