描述
紅-黑樹是特殊的二叉查找樹,又名R-B樹(RED-BLACK-TREE),由於紅黑樹是特殊的二叉查找樹,即紅黑樹具有了二叉查找樹的特性。
二叉查找樹(BST)是一顆二叉樹,其中每個節點都含有一個可比較的鍵(以及相關聯的值)且每個結點的鍵都大於其左子樹中的任意結點的鍵而小於右子樹的任意結點的鍵。
紅-黑樹也是一顆平衡樹(但又區別於完全的平衡二叉樹–AVL),對一個要插入的數據項(節點),插入過程會檢查會不會破壞樹的特徵,如果有破壞,會進行修正,根據需要改變數的結構,從而保持樹的平衡。紅黑樹的平均查找效率爲O(logN)。
紅-黑樹特徵及規則
特徵
- 節點都有顏色(兩種顏色)。
- 在插入和刪除的過程中,會遵循保持這些顏色的不同排列規則。
規則(基本性質)
- 每個節點的顏色爲紅色或黑色中的一種。
- 根節點總是黑色。
- 如果當前節點爲紅色,則它的子節點必須爲黑色,但反之則不一定。(即不能出現兩個連續的紅色節點)。
- 從根節點到葉子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)。
- 每個葉子節點(NIL)是黑色。 [注意:這裏葉子節點,是指爲空(NIL或NULL)的葉子節點!]
注意:新插入的節點顏色默認均爲紅色。
因爲依據紅黑樹規則,樹中的黑色節點數量是紅色節點數量的2倍多,插入一個紅色節點比插入一個黑色節點違背紅-黑樹規則的可能性(概率)更小。插入黑色節點總會改變黑色高度,但是插入紅色節點只有50%的概率會違背規則3,另外違背規則3要比違背規則4更容易修正。
紅黑樹修正處理方式
實現邏輯
首先要說明的是節點本身是不會旋轉的,旋轉改變的是節點之間的關係。
-
調整節點顏色
一般新插入顏色都爲紅色,那麼我們發現直接插入如果違反規則3,改爲黑色可能違反規則4。這時候我們將其父節點顏色改爲黑色,父節點的兄弟節點顏色也改爲黑色。 -
左旋
- 條件:插入節點的父節點爲紅色,叔叔節點爲黑色,且插入節點是父節點的右子節點。
- 步驟:
- 將y的左節點ly賦給x的右節點,並將x作爲y的左子節點的父節點,即將y的左子節點轉移到x的右子節點;
- 將y轉移到原x的位置,將x作爲y的左子節點,y作爲x的父節點。
-
右旋
- 條件:插入節點的父節點爲紅色,叔叔節點爲黑色,且插入節點是父節點的右子節點。
- 步驟:
- 將x的右節點rx賦給y的左子節點,將y作爲x的左子節點的父節點,即將x的右子節點轉移到y的左子節點;
- 將x轉移到原y的位置,將y作爲x的右子節點,x作爲y的父節點。
代碼實現
- 左旋
private void leftRotate(RBNode<T> x){
//1. 將y的左子節點賦給x的右子節點,並將x賦給y左子節點的父節點(y左子節點非空時)
RBNode<T> y = x.right;
x.right = y.left;
if(y.left != null){
y.left.parent = x;
}
//2. 將x的父節點p(非空時)賦給y的父節點,同時更新p的子節點爲y(左或右)
y.parent = x.parent;
//如果x的父節點爲空(即x爲根節點),則將y設爲根節點
if(x.parent == null){
this.root = y;
}else{
//如果x是左子節點,則也將y設爲左子節點,否則將y設爲右子節點
if(x == x.parent.left){
x.parent.left = y;
}else{
x.parent.right = y;
}
}
//3. 將y的左子節點設爲x,將x的父節點設爲y
y.left = x;
x.parent = y;
}
- 右旋
private void rightRotate(RBNode<T> y){
//1. 將y的左子節點賦給x的右子節點,並將x賦給y左子節點的父節點(y左子節點非空時)
RBNode<T> x = y.left;
y.left = x.right;
if(x.right != null){
x.right.parent = y;
}
//2. 將x的父節點p(非空時)賦給y的父節點,同時更新p的子節點爲y(左或右)
x.parent = y.parent;
if(y.parent == null){
//如果y的父節點爲空(即y爲根節點),則旋轉後將x設爲根節點
this.root = x;
}else{
//如果y是左子節點,則將x也設置爲左子節點,否則將x設置爲右子節點
if(y == y.parent.left){
y.parent.left = x;
}else{
y.parent.right = x;
}
}
//3. 將x的左子節點設爲y,將y的父節點設爲y
x.right = y;
y.parent = x;
}
插入操作
實現邏輯
- 先找到節點插入的位置
- 進行插入節點操作
- 平衡樹,進行修正處理(insertFixUp(node))
修正處理的情形與處理:
- 如果是第一次插入,由於原樹爲空,則只需將根節點顏色改爲黑色。–改變顏色
- 如果插入節點的父節點是黑色的,那不會違背紅-黑樹的規則,什麼也不需要做。–不需要處理
- 其他情形
- 插入節點的父節點和其叔叔節點(祖父節點的另一個子節點)均爲紅色。–改變顏色
- 插入節點的父節點是紅色的,叔叔節點是黑色的,且插入節點是其父節點的右子節點。–左旋
- 插入節點的父節點是紅色的,叔叔節點是黑色的,且插入節點是其父節點的左子節點。–右旋
代碼實現
public void insert(T key){
RBNode<T> node = new RBNode<T>(RED, key, null, null, null);
if(node != null){
insert(node);
}
}
public void insert(RBNode<T> node){
//表示最後node的父節點
RBNode<T> current = null;
//用來向下搜索
RBNode<T> x = this.root;
//1.找到插入位置
while(x != null){
current = x;
int cmp = node.key.compareTo(x.key);
if(cmp < 0){
x = x.left;
}else{
x = x.right;
}
}
//找到了插入的位置,將當前current作爲node的父節點
node.parent = current;
//2.接下來判斷node是左子節點還是右子節點
if(current != null){
int cmp = node.key.compareTo(current.key);
if(cmp < 0){
current.left = node;
}else{
current.right = node;
}
}else{
this.root = node;
}
//3.利用旋轉操作將其修正爲一顆紅黑樹
insertFixUp(node);
}
private void insertFixUp(RBNode<T> node){
//定義父節點和祖父節點
RBNode<T> parent,gparent;
//需要修正的條件:父節點存在,且父節點的顏色是紅色
while(((parent = parentOf(node)) != null) && isRed(parent)){
gparent = parentOf(parent);
//若父節點是祖父節點的左子節點,下面的else相反
if(parent == gparent.left){
//獲得叔叔節點
RBNode<T> uncle = gparent.right;
//case1:叔叔節點也是紅色
if(uncle != null && isRed(uncle)){
setBlack(parent);
setBlack(gparent);
setRed(gparent);
//把位置放到祖父節點處
node = gparent;
//繼續while循環,重新判斷
continue;
}
//case2:叔叔節點是黑色,且當前節點是右子節點
if(node == parent.right){
//從父節點出左旋
leftRotate(parent);
//然後將父節點和自己調換一下,爲下面右旋做準備
RBNode<T> tmp = parent;
parent = node;
node = tmp;
}
//case3:叔叔節點是黑色,且當前節點是左子節點
setBlack(parent);
setRed(gparent);
rightRotate(gparent);
}else{
//若父節點是祖父節點的右子節點,與上面的情況完全相反,本質是一樣的
RBNode<T> uncle = gparent.left;
//case1:叔叔節點也是紅色的
if(uncle != null && isRed(uncle)){
setBlack(parent);
setBlack(uncle);
setRed(gparent);
node = gparent;
continue;
}
//case2:叔叔節點是黑色的,且當前節點是左子節點
if(node == parent.left){
rightRotate(parent);
RBNode<T> tmp = parent;
parent = node;
node = tmp;
}
//case3:叔叔節點是黑色的,且當前節點是右子節點
setBlack(parent);
setRed(gparent);
leftRotate(gparent);
}
}
//將根節點設置爲黑色
setBlack(root);
}
總結
紅黑樹的查找、插入和刪除時間複雜度都爲O(),額外的開銷是每個節點的存儲空間都稍微增加了一點,因爲一個存儲紅黑樹節點的顏色變量。插入和刪除的時間要增加一個常數因子,因爲要進行旋轉,平均一次插入大約需要一次旋轉,因此插入的時間複雜度還是O()(時間複雜度的計算要省略常數),但實際上比普通的二叉樹是要慢的。
大多數應用中,查找的次數比插入和刪除的次數多,所以應用紅黑樹取代普通的二叉搜索樹總體上不會有太多的時間開銷。而且紅黑樹的優點是對於有序數據的操作不會慢到O(N)的時間複雜度。
參考文章
Java數據結構和算法(十一)——紅黑樹
JAVA實踐紅黑樹-小試牛刀
java數據結構與算法之平衡二叉樹(AVL樹)的設計與實現