Java數據結構和算法(十一)——紅黑樹


  上一篇博客我們介紹了二叉搜索樹,二叉搜索樹對於某個節點而言,其左子樹的節點關鍵值都小於該節點關鍵值,右子樹的所有節點關鍵值都大於該節點關鍵值。二叉搜索樹作為一種數據結構,其查找、插入和刪除操作的時間復雜度都為O(logn),底數為2。但是我們說這個時間復雜度是在平衡的二叉搜索樹上體現的,也就是如果插入的數據是隨機的,則效率很高,但是如果插入的數據是有序的,比如從小到大的順序【10,20,30,40,50】插入到二叉搜索樹中:

  

  從大到小就是全部在左邊,這和鏈表沒有任何區別了,這種情況下查找的時間復雜度為O(N),而不是O(logN)。當然這是在最不平衡的條件下,實際情況下,二叉搜索樹的效率應該在O(N)和O(logN)之間,這取決於樹的不平衡程度。

  那么為了能夠以較快的時間O(logN)來搜索一棵樹,我們需要保證樹總是平衡的(或者大部分是平衡的),也就是說每個節點的左子樹節點個數和右子樹節點個數盡量相等。紅-黑樹的就是這樣的一棵平衡樹,對一個要插入的數據項(刪除也是),插入例程要檢查會不會破壞樹的特征,如果破壞了,程序就會進行糾正,根據需要改變樹的結構,從而保持樹的平衡。

1、紅-黑樹的特征

  有如下兩個特征:

  ①、節點都有顏色;

  ②、在插入和刪除的過程中,要遵循保持這些顏色的不同排列規則。

  第一個很好理解,在紅-黑樹中,每個節點的顏色或者是黑色或者是紅色的。當然也可以是任意別的兩種顏色,這里的顏色用於標記,我們可以在節點類Node中增加一個boolean型變量isRed,以此來表示顏色的信息。

  第二點,在插入或者刪除一個節點時,必須要遵守的規則稱為紅-黑規則:

  1.每個節點不是紅色就是黑色的;

  2.根節點總是黑色的;

  3.如果節點是紅色的,則它的子節點必須是黑色的(反之不一定),(也就是從每個葉子到根的所有路徑上不能有兩個連續的紅色節點);

  4.從根節點到葉節點或空子節點的每條路徑,必須包含相同數目的黑色節點(即相同的黑色高度)。

  從根節點到葉節點的路徑上的黑色節點的數目稱為黑色高度,規則 4 另一種表示就是從根到葉節點路徑上的黑色高度必須相同。

  注意:新插入的節點顏色總是紅色的,這是因為插入一個紅色節點比插入一個黑色節點違背紅-黑規則的可能性更小,原因是插入黑色節點總會改變黑色高度(違背規則4),但是插入紅色節點只有一半的機會會違背規則3(因為父節點是黑色的沒事,父節點是紅色的就違背規則3)。另外違背規則3比違背規則4要更容易修正。當插入一個新的節點時,可能會破壞這種平衡性,那么紅-黑樹是如何修正的呢?

2、紅-黑樹的自我修正

  紅-黑樹主要通過三種方式對平衡進行修正,改變節點顏色、左旋和右旋。

  ①、改變節點顏色

  

  新插入的節點為15,一般新插入顏色都為紅色,那么我們發現直接插入會違反規則3,改為黑色卻發現違反規則4。這時候我們將其父節點顏色改為黑色,父節點的兄弟節點顏色也改為黑色。通常其祖父節點50顏色會由黑色變為紅色,但是由於50是根節點,所以我們這里不能改變根節點顏色。

  ②、右旋

  首先要說明的是節點本身是不會旋轉的,旋轉改變的是節點之間的關系,選擇一個節點作為旋轉的頂端,如果做一次右旋,這個頂端節點會向下和向右移動到它右子節點的位置,它的左子節點會上移到它原來的位置。右旋的頂端節點必須要有左子節點。

  

  ③、左旋

  左旋的頂端節點必須要有右子節點。

  

   注意:我們改變顏色也是為了幫助我們判斷何時執行什么旋轉,而旋轉是為了保證樹的平衡。光改變節點顏色是不能起到任何作用的,旋轉才是關鍵的操作,在新增節點或者刪除節點之后,可能會破壞二叉樹的平衡,那么何時執行旋轉以及執行什么旋轉,這是我們需要重點關注的。

3、左旋和右旋代碼

  ①、節點類

  節點類和二叉樹的節點類差不多,只不過在其基礎上增加了一個 boolean 類型的變量來表示節點的顏色。

public class RBNode<T extends Comparable<T>> {
	boolean color;//顏色
	T key;//關鍵值
	RBNode<T> left;//左子節點
	RBNode<T> right;//右子節點
	RBNode<T> parent;//父節點
	
	public RBNode(boolean color,T key,RBNode<T> parent,RBNode<T> left,RBNode<T> right){
		this.color = color;
		this.key = key;
		this.parent = parent;
		this.left = left;
		this.right = right;
	}
	
	//獲得節點的關鍵值
	public T getKey(){
		return key;
	}
	//打印節點的關鍵值和顏色信息
	public String toString(){
		return ""+key+(this.color == RED ? "R":"B");
	}
}

  ②、左旋的具體實現

/*************對紅黑樹節點x進行左旋操作 ******************/
/* 
 * 左旋示意圖:對節點x進行左旋 
 *     p                       p 
 *    /                       / 
 *   x                       y 
 *  / \                     / \ 
 * lx  y      ----->       x  ry 
 *    / \                 / \ 
 *   ly ry               lx ly 
 * 左旋做了三件事: 
 * 1. 將y的左子節點賦給x的右子節點,並將x賦給y左子節點的父節點(y左子節點非空時) 
 * 2. 將x的父節點p(非空時)賦給y的父節點,同時更新p的子節點為y(左或右) 
 * 3. 將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;
	if(x.parent == null){
		this.root = y;//如果x的父節點為空(即x為根節點),則將y設為根節點
	}else{
		if(x == x.parent.left){//如果x是左子節點
			x.parent.left = y;//則也將y設為左子節點  
		}else{
			x.parent.right = y;//否則將y設為右子節點  
		}
	}
	
	//3. 將y的左子節點設為x,將x的父節點設為y
	y.left = x;
	x.parent = y;
}

  ③、右旋的具體實現  

/*************對紅黑樹節點y進行右旋操作 ******************/  
/* 
 * 左旋示意圖:對節點y進行右旋 
 *        p                   p 
 *       /                   / 
 *      y                   x 
 *     / \                 / \ 
 *    x  ry   ----->      lx  y 
 *   / \                     / \ 
 * lx  rx                   rx ry 
 * 右旋做了三件事: 
 * 1. 將x的右子節點賦給y的左子節點,並將y賦給x右子節點的父節點(x右子節點非空時) 
 * 2. 將y的父節點p(非空時)賦給x的父節點,同時更新p的子節點為x(左或右) 
 * 3. 將x的右子節點設為y,將y的父節點設為x 
 */
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){
		this.root = x;//如果y的父節點為空(即y為根節點),則旋轉后將x設為根節點
	}else{
		if(y == y.parent.left){//如果y是左子節點
			y.parent.left = x;//則將x也設置為左子節點
		}else{
			y.parent.right = x;//否則將x設置為右子節點
		}
	}
	
	//3. 將x的左子節點設為y,將y的父節點設為y
	x.right = y;
	y.parent = x;
}

4、插入操作

  和二叉樹的插入操作一樣,都是得先找到插入的位置,然后再將節點插入。先看看插入的前段代碼:

/*********************** 向紅黑樹中插入節點 **********************/
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){
	RBNode<T> current = null;//表示最后node的父節點
	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;
		}
	}
	node.parent = current;//找到了插入的位置,將當前current作為node的父節點
	
	//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);
}

  這與二叉搜索樹中實現的思路一樣,這里不再贅述,主要看看方法里面最后一步insertFixUp(node)操作。因為插入后可能會導致樹的不平衡,insertFixUp(node) 方法里主要是分情況討論,分析何時變色,何時左旋,何時右旋。我們先從理論上分析具體的情況,然后再看insertFixUp(node) 的具體實現。

  如果是第一次插入,由於原樹為空,所以只會違反紅-黑樹的規則2,所以只要把根節點塗黑即可;如果插入節點的父節點是黑色的,那不會違背紅-黑樹的規則,什么也不需要做;但是遇到如下三種情況,我們就要開始變色和旋轉了:

  ①、插入節點的父節點和其叔叔節點(祖父節點的另一個子節點)均為紅色。

  ②、插入節點的父節點是紅色的,叔叔節點是黑色的,且插入節點是其父節點的右子節點。

  ③、插入節點的父節點是紅色的,叔叔節點是黑色的,且插入節點是其父節點的左子節點。

  下面我們挨個分析這三種情況都需要如何操作,然后給出實現代碼。

  在下面的討論中,使用N,P,G,U表示關聯的節點。N(now)表示當前節點,P(parent)表示N的父節點,U(uncle)表示N的叔叔節點,G(grandfather)表示N的祖父節點,也就是P和U的父節點。

  對於情況1:插入節點的父節點和其叔叔節點(祖父節點的另一個子節點)均為紅色。此時,肯定存在祖父節點,但是不知道父節點是其左子節點還是右子節點,但是由於對稱性,我們只要討論出一邊的情況,另一種情況自然也與之對應。這里考慮父節點是其祖父節點的左子節點的情況,如下左圖所示:

           

 

   對於這種情況,我們要做的操作有:將當前節點(4) 的父節點(5) 和叔叔節點(8) 塗黑,將祖父節點(7)塗紅,變成了上有圖所示的情況。再將當前節點指向其祖父節點,再次從新的當前節點開始算法(具體看下面的步驟)。這樣上右圖就變成情況2了。

  對於情況2:插入節點的父節點是紅色的,叔叔節點是黑色的,且插入節點是其父節點的右子節點。我們要做的操作有:將當前節點(7)的父節點(2)作為新的節點,以新的當前節點為支點做左旋操作。完成后如左下圖所示,這樣左下圖就變成情況3了。

       

 

   對於情況3:插入節點的父節點是紅色,叔叔節點是黑色,且插入節點是其父節點的左子節點。我們要做的操作有:將當前節點的父節點(7)塗黑,將祖父節點(11)塗紅,在祖父節點為支點做右旋操作。最后把根節點塗黑,整個紅-黑樹重新恢復了平衡,如右上圖所示。至此,插入操作完成!

  我們可以看出,如果是從情況1開始發生的,必然會走完情況2和3,也就是說這是一整個流程,當然咯,實際中可能不一定會從情況1發生,如果從情況2開始發生,那再走個情況3即可完成調整,如果直接只要調整情況3,那么前兩種情況均不需要調整了。故變色和旋轉之間的先后關系可以表示為:變色->左旋->右旋。

  至此,我們完成了全部的插入操作。下面我們看看insertFixUp方法中的具體實現(可以結合上面的分析圖,更加利與理解):

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;//把位置放到祖父節點處
				continue;//繼續while循環,重新判斷
			}
			
			//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);//將根節點設置為黑色
}

5、刪除操作

  上面探討完了紅-黑樹的插入操作,接下來討論刪除,紅-黑樹的刪除和二叉查找樹的刪除是一樣的,只不過刪除后多了個平衡的修復而已。我們先來回憶一下二叉搜索樹的刪除:

  ①、如果待刪除的節點沒有子節點,那么直接刪除即可。

  ②、如果待刪除的節點只有一個子節點,那么直接刪掉,並用其子節點去頂替它。

  ③、如果待刪除的節點有兩個子節點,這種情況比較復雜:首先找出它的后繼節點,然后處理“后繼節點”和“被刪除節點的父節點”之間的關系,最后處理“后繼節點的子節點”和“被刪除節點的子節點”之間的關系。每一步中也會有不同的情況。

  實際上,刪除過程太復雜了,很多情況下會采用在節點類中添加一個刪除標記,並不是真正的刪除節點。詳細的刪除我們這里不做討論。

6、紅黑樹的效率

  紅黑樹的查找、插入和刪除時間復雜度都為O(log2N),額外的開銷是每個節點的存儲空間都稍微增加了一點,因為一個存儲紅黑樹節點的顏色變量。插入和刪除的時間要增加一個常數因子,因為要進行旋轉,平均一次插入大約需要一次旋轉,因此插入的時間復雜度還是O(log2N),(時間復雜度的計算要省略常數),但實際上比普通的二叉樹是要慢的。

  大多數應用中,查找的次數比插入和刪除的次數多,所以應用紅黑樹取代普通的二叉搜索樹總體上不會有太多的時間開銷。而且紅黑樹的優點是對於有序數據的操作不會慢到O(N)的時間復雜度。

  參考文檔:http://blog.csdn.net/eson_15/article/details/51144079  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM