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  

相关文章
相关标签/搜索