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

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

  

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

  那麼爲了可以以較快的時間O(logN)來搜索一棵樹,咱們須要保證樹老是平衡的(或者大部分是平衡的),也就是說每一個節點的左子樹節點個數和右子樹節點個數儘可能相等。紅-黑樹的就是這樣的一棵平衡樹,對一個要插入的數據項(刪除也是),插入例程要檢查會不會破壞樹的特徵,若是破壞了,程序就會進行糾正,根據須要改變樹的結構,從而保持樹的平衡。算法

一、紅-黑樹的特徵

  有以下兩個特徵:數據結構

  ①、節點都有顏色;this

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

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

  第二點,在插入或者刪除一個節點時,必需要遵照的規則稱爲紅-黑規則:3d

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

  2.根節點老是黑色的;文檔

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

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

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

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

二、紅-黑樹的自我修正

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

  ①、改變節點顏色

  

  新插入的節點爲15,通常新插入顏色都爲紅色,那麼咱們發現直接插入會違反規則3,改成黑色卻發現違反規則4。這時候咱們將其父節點顏色改成黑色,父節點的兄弟節點顏色也改成黑色。一般其祖父節點50顏色會由黑色變爲紅色,可是因爲50是根節點,因此咱們這裏不能改變根節點顏色。

  ②、右旋

  首先要說明的是節點自己是不會旋轉的,旋轉改變的是節點之間的關係,選擇一個節點做爲旋轉的頂端,若是作一次右旋,這個頂端節點會向下和向右移動到它右子節點的位置,它的左子節點會上移到它原來的位置。右旋的頂端節點必需要有左子節點。

  

  ③、左旋

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

  

   注意:咱們改變顏色也是爲了幫助咱們判斷什麼時候執行什麼旋轉,而旋轉是爲了保證樹的平衡。光改變節點顏色是不能起到任何做用的,旋轉纔是關鍵的操做,在新增節點或者刪除節點以後,可能會破壞二叉樹的平衡,那麼什麼時候執行旋轉以及執行什麼旋轉,這是咱們須要重點關注的。

三、左旋和右旋代碼

  ①、節點類

  節點類和二叉樹的節點類差很少,只不過在其基礎上增長了一個 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;
}

四、插入操做

  和二叉樹的插入操做同樣,都是得先找到插入的位置,而後再將節點插入。先看看插入的前段代碼:

/*********************** 向紅黑樹中插入節點 **********************/
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);//將根節點設置爲黑色
}

五、刪除操做

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

  ①、若是待刪除的節點沒有子節點,那麼直接刪除便可。

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

  ③、若是待刪除的節點有兩個子節點,這種狀況比較複雜:首先找出它的後繼節點,而後處理「後繼節點」和「被刪除節點的父節點」之間的關係,最後處理「後繼節點的子節點」和「被刪除節點的子節點」之間的關係。每一步中也會有不一樣的狀況。

  實際上,刪除過程太複雜了,不少狀況下會採用在節點類中添加一個刪除標記,並非真正的刪除節點。詳細的刪除咱們這裏不作討論。

六、紅黑樹的效率

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

  大多數應用中,查找的次數比插入和刪除的次數多,因此應用紅黑樹取代普通的二叉搜索樹整體上不會有太多的時間開銷。並且紅黑樹的優勢是對於有序數據的操做不會慢到O(N)的時間複雜度。

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

相關文章
相關標籤/搜索