演算法導論學習筆記—紅黑樹最全的Java實現
紅黑樹(red-black-tree)是許多“平衡”搜尋樹的一種,它可以保證在最壞情況下基本動態集合操作的時間複雜度為O(lgn)。除遍歷外,其餘的方法的時間複雜度都為O(lgn),如INSERT, SEARCH, MAXIMUM, MINIMUM, DELETE等。本章 將依次介紹一些比較重要的方法,並賦予其Java程式碼的實現。詳細的紅黑樹理論,可以參考《演算法導論》中P174-192。
注:下面幾篇博文值得借鑑,本文在此基礎上做了一些修改和完善。
https://www.cnblogs.com/skywang12345/p/3624343.html
http://blog.csdn.net/goodluckwhh/article/details/12718233
如果有問題,歡迎一起討論。QQ: 724735573
首先,定義RBNode的屬性
class RBNode{
public RBNode leftNode; //左子結點
public RBNode rightNode; //右子結點
public RBNode parent; //父結點
public int data; //元資料
public boolean color; //顏色
public RBNode(int data) {
this.data=data;
}
下面,將依次介紹幾個重要方法的Java實現。
(1) 左旋 LeftRotate
/*
* 對紅黑樹的節點(x)進行左旋轉
*
* px px
* / /
* x y
* / \ --(左旋)-. / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
*
*/
public void leftRotate(RBNode x) {
RBNode y = x.rightNode;
//System.out.println(y.data);
// 如果y的左孩子非空,將 “x” 設為 “y的左孩子的父親”
// 將 “y的左孩子” 設為 “x的右孩子”;
if (y.leftNode != null) {
y.leftNode.parent = x;
}
x.rightNode=y.leftNode;
RBNode px = x.parent;
y.parent=px;
if(px==null) {
root = y;
}else {
if(px.leftNode==x) {
px.leftNode=y;
}else {
px.rightNode=y;
}
}
// 將 “x” 設為 “y的左孩子”
y.leftNode=x;
// 將 “x的父節點” 設為 “y”
x.parent=y;
}
(2)右旋 RightRotate
/*
* 對紅黑樹的節點(y)進行右旋轉
*
* py py
* / /
* y x
* / \ --(右旋)-. / \
* x ry lx y
* / \ / \
* lx rx rx ry
*
*/
public void rightRotate(RBNode y) {
RBNode x = y.leftNode;
if(x.rightNode!=null) {
x.rightNode.parent=y;
}
y.leftNode=x.rightNode;
RBNode py = y.parent;
x.parent=py;
if(py==null) {
root = x;
}else {
if(py.leftNode==y) {
py.leftNode=x;
}else {
py.rightNode=x;
}
}
// 將 “y” 設為 “x的右孩子”
y.parent=x;
// 將 “y的父節點” 設為 “x”
x.rightNode=y;
}
(3)插入 Insert
第一步: 將紅黑樹當作一顆二叉查詢樹,將節點插入。
紅黑樹本身就是一顆二叉查詢樹,將節點插入後,該樹仍然是一顆二叉查詢樹。也就意味著,樹的鍵值仍然是有序的。此外,無論是左旋還是右旋,若旋轉之前這棵樹是二叉查詢樹,旋轉之後它一定還是二叉查詢樹。這也就意味著,任何的旋轉和重新著色操作,都不會改變它仍然是一顆二叉查詢樹的事實。
那接下來,我們就來想方設法的旋轉以及重新著色,使這顆樹重新成為紅黑樹!
第二步:將插入的節點著色為"紅色"。
為什麼著色成紅色,而不是黑色呢?為什麼呢?在回答之前,我們需要重新溫習一下紅黑樹的特性:
(1) 每個節點或者是黑色,或者是紅色。
(2) 根節點是黑色。
(3) 每個葉子節點是黑色。 [注意:這裡葉子節點,是指為空的葉子節點!]
(4) 如果一個節點是紅色的,則它的子節點必須是黑色的。
(5) 從一個節點到該節點的子孫節點的所有路徑上包含相同數目的黑節點。
將插入的節點著色為紅色,不會違背"特性(5)"!少違背一條特性,就意味著我們需要處理的情況越少。接下來,就要努力的讓這棵樹滿足其它性質即可;滿足了的話,它 就又是一顆紅黑樹了。
第三步: 通過一系列的旋轉或著色等操作,使之重新成為一顆紅黑樹。
第二步中,將插入節點著色為"紅色"之後,不會違背"特性(5)"。那它到底會違背哪些特性呢?
對於"特性(1)",顯然不會違背了。因為我們已經將它塗成紅色了。
對於"特性(2)",顯然也不會違背。在第一步中,我們是將紅黑樹當作二叉查詢樹,然後執行的插入操作。而根據二叉查詢數的特點,插入操作不會改變根節點。所以,根 節點仍然是黑色。
對於"特性(3)",顯然不會違背了。這裡的葉子節點是指的空葉子節點,插入非空節點並不會對它們造成影響。
對於"特性(4)",是有可能違背的!
那接下來,想辦法使之"滿足特性(4)",就可以將樹重新構造成紅黑樹了。
/*
* 紅黑樹的插入操作的時間複雜度為O(lgn),因為正常的二叉樹的插入時間複雜度為O(lgn),而inserFix的時間複雜度也為O(lgn), 所以綜述總的時間複雜度為O(lgn)
*/
public void insert(RBNode node) {
RBNode current = root;
RBNode parent = root;
// 1. 將紅黑樹當作一顆二叉查詢樹,將節點新增到二叉查詢樹中。
if(root == null) {
root = node;
root.parent=null;
}else {
while(true) {
parent = current ;
if(current.data>node.data) {
current = current.leftNode;
if(current==null) {
parent.leftNode=node;
node.parent=parent;
break;
}
}else {
current = current.rightNode;
if(current==null) {
parent.rightNode=node;
node.parent=parent;
break;
}
}
}
}
// 2. 設定節點的顏色為紅色
node.color=RED;
// 3. 將它重新修正為一顆二叉查詢樹
insertFixUp(node);
}
(4)插入修改 InsertFixUp
private void insertFixUp(RBNode node) {
RBNode parent =null;
RBNode gparent = null;
// 若“父節點存在,並且父節點的顏色是紅色”
while((parent=node.parent)!=null && parent.color==RED) {
gparent = parent.parent;
//若“父節點”是“祖父節點的左孩子”
if(gparent!=null && parent == gparent.leftNode) {
RBNode uncle = gparent.rightNode;
// Case 1條件:叔叔節點是紅色
if(uncle !=null && uncle.color==RED) {
uncle.color=BLACK;
parent.color=BLACK;
gparent.color=RED;
node = gparent;
continue;
}
// Case 2條件:叔叔是黑色,且當前節點是右孩子
if( (uncle==null || uncle.color==BLACK) && (parent.rightNode==node)) {
node = parent;
leftRotate(parent);
continue;
}
// Case 3條件:叔叔是黑色,且當前節點是左孩子。
parent.color=BLACK;
gparent.color=RED;
rightRotate(gparent);
}else {
//若“z的父節點”是“z的祖父節點的右孩子”
RBNode uncle = gparent.leftNode;
// Case 1條件:叔叔節點是紅色
if(uncle !=null && uncle.color==RED) {
uncle.color=BLACK;
parent.color=BLACK;
gparent.color=RED;
node = gparent;
continue;
}
// Case 2條件:叔叔是黑色,且當前節點是左孩子
if( (uncle==null || uncle.color==BLACK) && (parent.leftNode==node)) {
node = parent;
rightRotate(parent);
continue;
}
// Case 3條件:叔叔是黑色,且當前節點是右孩子。
parent.color=BLACK;
gparent.color=RED;
leftRotate(gparent);
}
}
root.color=BLACK;
}
(5)查詢 Search
public RBNode search(int value) {
RBNode current = root;
while(current !=null) {
if(current.data==value) {
break;
}
if(current.data>value) {
current = current.leftNode;
}else{
current = current.rightNode;
}
}
return current;
}
(6)尋找中序後繼結點 Successor
//找到中序後繼結點C,並解除C節點與其父結點的關係
public RBNode getSuccessor2(RBNode node) {
RBNode parent = node;
RBNode grandParent = node;
RBNode current = node.rightNode;
while(current!=null) {
grandParent = parent;
parent = current;
current = current.leftNode;
}
if(parent!=node.rightNode) {
grandParent.leftNode=parent.rightNode;
if(parent.rightNode!=null) {
parent.rightNode.parent=grandParent;
}
}else {
grandParent.rightNode=parent.rightNode;
if(parent.rightNode!=null) {
parent.rightNode.parent=grandParent;
}
}
return parent;
}
(7)刪除 REMOVE
從排序樹中刪除節點的思路是一樣的,首先找到要刪除的節點,並做如下處理:
- 如果該節點不存在非空子節點,則直接刪除它
- 如果該節點存在一個非空子節點,則用其非空子節點替換其位置即可
- 如果該節點有兩個非空子節點,則可以找到該節點的前驅或者後繼,然後更換兩個節點的元素值,再將前驅或者後繼當做被刪除的節點(由於任意一個節點的前驅或者後繼都必定至多隻有一個非空子節點,因而刪除這樣的節點就可以按照前兩種情形進行處理)
- 如果節點A有兩個非空子節點,則找到節點A的前驅(也可以是後繼),然後記要刪除的元素B=A的前驅
- 否則只要A有一個空子節點,就記要刪除的元素B=A
- 記變數X為B的第一個非空子節點(先檢查左孩子,後檢查右孩子),如果B的兩個孩子都為空,則記X為空
- 如果B和A不同,則將B的內容拷貝到A
- 刪除節點B
- 如果被刪除節點B的顏色為紅色,則刪除結束,否則從節點X處開始對刪除節點後的紅黑樹進行調整
- 如果要刪除節點75,則A=75,B=75,X=85
- 如果要刪除節點25,則A=25,B=29,X=NULL
- 如果要刪除節點40,則A=B=40,X=NULL
顯然,在這種處理方式下,如果B的顏色為紅色,則刪除它後刪除操作即完成了,因為:
- B是紅色的,因而它不是根
- B是紅色的,它的父節點必然是黑色的,因而刪除它不會引入兩個紅色的連續節點
- B是紅色的,而且它至多有一個非空子節點,因而刪除它不會導致紅黑樹的任意路徑上的黑節點數目變化
/*
* 紅黑樹的刪除操作的時間複雜度為O(lgn),因為正常的二叉樹的刪除時間複雜度為O(lgn),而removeFix的時間複雜度也為O(lgn)
所以綜述總的時間複雜度為O(lgn)
*/
public int rbRemove(int value) {
//定義結點A,B,X
RBNode A,B,X,parent;
boolean color;
//先找到被刪除的結點
A = search(value);
if(A ==null) {
return -1;
}
//如果被刪除結點的左子節點和右子節點都不為空,找到中序後繼結點,然後記要刪除的元素B=A的後繼
if(A.leftNode!=null && A.rightNode!=null) {
//取得該節點的中序後繼結點successor
B=getSuccessor2(A);
if(B==A.rightNode) {
parent = B;
}else {
parent = B.parent;
}
color=B.color;
//賦值X,X為B的第一個非空子節點(先檢查左孩子,後檢查右孩子),如果B的兩個孩子都為空,則記X為空
if(B.leftNode!=null) {
X=B.leftNode;
}else if(B.rightNode!=null){
X=B.rightNode;
}else {
X=null;
}
if(A==root) {
//B的左子節點為根節點的左節點
B.leftNode=root.leftNode;
root.leftNode.parent=B;
//將根節點的右節點賦給B的右節點
B.rightNode=root.rightNode;
root.rightNode.parent=B;
//替換根節點
root=B;
}else {
//A在左邊
if(A.parent.leftNode==A) {
A.parent.leftNode=B;
B.parent=A.parent;
//將A節點的右節點賦給B的右節點
B.leftNode=A.leftNode;
A.leftNode.parent=B;
//將A節點的右節點賦給B的右節點
B.rightNode=A.rightNode;
if(A.rightNode!=null) {
A.rightNode.parent=B;
}
}else {
//A在右邊
A.parent.rightNode=B;
B.parent=A.parent;
//將A節點的右節點賦給B的右節點
B.leftNode=A.leftNode;
A.leftNode.parent=B;
//將A節點的右節點賦給B的右節點
B.rightNode=A.rightNode;
if(A.rightNode!=null) {
A.rightNode.parent=B;
}
}
}
}else {
//否則只要被刪除結點有一個空節點,記B=A
B=A;
parent=B.parent;
color = B.color;
//賦值X,X為B的第一個非空子節點(先檢查左孩子,後檢查右孩子),如果B的兩個孩子都為空,則記X為空
if(B.leftNode!=null) {
X=B.leftNode;
}else if(B.rightNode!=null){
X=B.rightNode;
}else {
X=null;
}
//刪除B
//首先如果B為一個葉子結點
if(B.leftNode==null && B.rightNode==null) {
//如果B為根節點
if(B==root) {
root =null;
}else {
//B在左邊
if(B.parent.leftNode==B) {
B.parent.leftNode=null;
B.parent=null;
}else {
//B在右邊
B.parent.rightNode=null;
B.parent=null;
}
}
}else {
//刪除的結點為只有一個子節點的父節點
if(B==root) {
//如果刪除的結點為root
if(root.leftNode!=null) {
root = root.leftNode;
root.leftNode.parent=null;
}else {
root = root.rightNode;
root.rightNode.parent=null;
}
}else {
//如果刪除的不是root結點
//如果B在左邊
if(B.parent.leftNode==B) {
//左子節點不為空
if(B.leftNode!=null) {
B.parent.leftNode=B.leftNode;
B.leftNode.parent=B.parent;
}else {
//右子節點不為空
B.parent.leftNode=B.rightNode;
B.rightNode.parent=B.parent;
}
}else {
//如果B在右邊
if(B.leftNode!=null) {
B.parent.rightNode=B.leftNode;
B.leftNode.parent=B.parent;
}else {
B.parent.rightNode=B.rightNode;
B.rightNode.parent=B.parent;
}
}
}
}
}
//如果被刪除節點B的顏色為紅色,則刪除結束,否則從節點X處開始對刪除節點後的紅黑樹進行調整
if(color==BLACK) {
removeFix(X,parent);
}
return A.data;
}
(8)刪除的調整 RemoveFix
public void removeFix(RBNode node,RBNode parent) {
//如果N結點為根節點或者N的顏色是紅色,就將其顏色設為黑色
RBNode s;
if(node!=null && (node == root || node.color==RED)) {
node.color=BLACK;
return ;
}
while((node==null || node.color==BLACK) && node!=root) {
if(parent.leftNode==node) {
s=parent.rightNode;
//case1: 如果兄弟結點是紅色
if(s.color==RED) {
s.color=BLACK;
parent.color=RED;
leftRotate(parent);
s=parent.rightNode;
}
//case2: node的兄弟s是黑色,且s的倆個孩子也都是黑色的
if((s.leftNode==null || s.leftNode.color==BLACK ) && (s.rightNode==null || s.rightNode.color==BLACK )) {
s.color=RED;
node = parent;
}else {
//case3: node的兄弟s是黑色的,並且s的左孩子是紅色,右孩子為黑色。
if(s.rightNode==null ||s.rightNode.color==BLACK) {
s.leftNode.color=BLACK;
s.color=RED;
rightRotate(s);
s=parent.rightNode;
}
// Case 4: node的兄弟s是黑色的;並且s的右孩子是紅色的,左孩子任意顏色。
s.color=parent.color;
parent.color=BLACK;
s.rightNode.color=BLACK;
leftRotate(parent);
node=root;
}
}else {
s=parent.leftNode;
//case1:如果兄弟結點是紅色
if(s.color==RED) {
s.color=BLACK;
parent.color=RED;
rightRotate(parent);
s=parent.leftNode;
}
//case2: node的兄弟s是黑色,且s的倆個孩子也都是黑色的
if((s.leftNode==null || s.leftNode.color==BLACK ) && (s.rightNode==null || s.rightNode.color==BLACK )) {
s.color=RED;
node = parent;
}else {
//case3: node的兄弟s是黑色的,並且s的左孩子是紅色,右孩子為黑色。
if(s.rightNode==null ||s.rightNode.color==BLACK) {
s.rightNode.color=BLACK;
s.color=RED;
leftRotate(s);
s=parent.rightNode;
}
// Case 4: x的兄弟w是黑色的;並且w的右孩子是紅色的,左孩子任意顏色。
s.color=parent.color;
parent.color=BLACK;
s.leftNode.color=BLACK;
rightRotate(parent);
node=root;
}
}
}
//node.color=BLACK; //這裡和前面的第一個if意思一致
}
(9)前序遍歷FrontIterator
public void FrontIterator(RBNode node) {
if(node==null) {
return ;
}else {
System.out.print(node.data+"("+node.color+")"+" ");
FrontIterator(node.leftNode);
FrontIterator(node.rightNode);
}
}
(10)中序遍歷MiddleIterator
public void MiddleIterator(RBNode node) {
if(node==null) {
return ;
}else {
MiddleIterator(node.leftNode);
System.out.print(node.data+" ");
MiddleIterator(node.rightNode);
}
}
(11)後序遍歷LastIterator
public void LastIterator(RBNode node) {
if(node==null) {
return ;
}else {
LastIterator(node.leftNode);
LastIterator(node.rightNode);
System.out.print(node.data+" ");
}
}
(12)找最大值Maximum
public RBNode getMaximum() {
RBNode current = root;
while(current.rightNode!=null) {
current = current.rightNode;
}
return current;
}
(13)找最小值Minimum
public RBNode getMinimum() {
RBNode current = root;
while(current.leftNode!=null) {
current = current.leftNode;
}
return current;
}
(14)將陣列轉化成紅黑樹
public void generateArrayToRBTree(int[] arr) {
for(int i=0;i<arr.length;i++) {
insert(new RBNode(arr[i]));
}
}
(15)測試方法
public static void main(String[] args) {
RBTree tree = new RBTree();
//int[] arr = {11,2,14,1,7,5,8,4,15};
//41,38,31,12,19,8
int[] arr2 = {41,38,31,12,19,8};
//測試插入方法
tree.generateArrayToRBTree(arr2);
tree.FrontIterator(tree.root);
System.out.println();
//測試刪除方法
// int a = tree.rbRemove(7);
// System.out.println(a);
// tree.FrontIterator(tree.root);
// System.out.println();
}