红黑树Red-Black tree初步详解(Java代码实现)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/li2327234939/article/details/75628448

红黑树Red-Blacktree初步详解

本博客的参考资料:

算法导论

http://blog.csdn.net/v_july_v/article/details/6105630

http://www.cnblogs.com/skywang12345/p/3624343.html

一、红黑树简介

先来看下算法导论对R-B Tree的介绍:
红黑树,一种二叉查找树,但在每个结点上增加一个存储位表示结点的颜色,可以是RedBlack
通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。

 

红黑树,作为一棵二叉查找树,满足二叉查找树的一般性质。下面,来了解下二叉查找树的一般性质。

二叉查找树

         二叉查找树,也称有序二叉树(ordered binary tree),或已排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  • 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 任意节点的左、右子树也分别为二叉查找树。
  • 没有键值相等的节点(no duplicate nodes)。

         因为一棵由n个结点随机构造的二叉查找树的高度为lgn,所以顺理成章,二叉查找树的一般操作的执行时间为O(lgn)。但二叉查找树若退化成了一棵具有n个结点的线性链后,则这些操作最坏情况运行时间为O(n)

         红黑树虽然本质上是一棵二叉查找树,但它在二叉查找树的基础上增加了着色和相关的性质使得红黑树相对平衡,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)

         但它是如何保证一棵n个结点的红黑树的高度始终保持在logn的呢?这就引出了红黑树的5个性质:

1.    每个结点要么是红的要么是黑的。  

2.    根结点是黑的。  

3.    每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。  

4.    如果一个结点是红的,那么它的两个儿子都是黑的。  

5.     对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。 

         正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了logn的高度,从而也就解释了上面所说的红黑树的查找、插入、删除的时间复杂度最坏为O(log n)”这一结论成立的原因。

:上述第35点性质中所说的NULL结点,包括wikipedia.算法导论上所认为的叶子结点即为树尾端的NIL指针,或者说NULL结点。然百度百科以及网上一些其它博文直接说的叶结点,则易引起误会,因,此叶结点非子结点

如下图所示,即是一颗红黑树(下图引自wikipediahttp://t.cn/hgvH1l)


此图忽略了叶子和根部的父结点。同时,上文中我们所说的 "叶结点""NULL结点",如上图所示,它不包含数据而只充当树在此结束的指示,这些节点在绘图中经常被省略,望看到此文后的读者朋友注意。 

二、红黑树的时间复杂度和相关证明

红黑树的时间复杂度为: O(lgn)
下面通过“数学归纳法”对红黑树的时间复杂度进行证明。

定理:一棵含有n个节点的红黑树的高度至多为2log(n+1).

证明:
    "一棵含有n个节点的红黑树的高度至多为2log(n+1)" 的逆否命题是 "高度为h的红黑树,它的包含的内节点个数至少为 2h/2-1个"。
    我们只需要证明逆否命题,即可证明原命题为真;即只需证明 "高度为h的红黑树,它的包含的内节点个数至少为 2h/2-1"

    从某个节点x出发(不包括该节点)到达一个叶节点的任意一条路径上,黑色节点的个数称为该节点的黑高度(x's black height),记为bh(x)。关于bh(x)有两点需要说明: 
    第1点:根据红黑树的"特性(5) ,即从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点"可知,从节点x出发到达的所有的叶节点具有相同数目的黑节点。这也就意味着,bh(x)的值是唯一的
    第2点:根据红黑色的"特性(4),即如果一个节点是红色的,则它的子节点必须是黑色的"可知,从节点x出发达到叶节点"所经历的黑节点数目">= "所经历的红节点的数目"。假设x是根节点,则可以得出结论"bh(x) >= h/2"。进而,我们只需证明 "高度为h的红黑树,它的包含的黑节点个数至少为 2bh(x)-1"即可。

    到这里,我们将需要证明的定理已经由
"一棵含有n个节点的红黑树的高度至多为2log(n+1)" 
    转变成只需要证明
"高度为h的红黑树,它的包含的内节点个数至少为 2bh(x)-1"。


下面通过"数学归纳法"开始论证高度为h的红黑树,它的包含的内节点个数至少为 2bh(x)-1个"。

(01) 当树的高度h=0时,
    内节点个数是0,bh(x) 为0,2bh(x)-1 也为 0。显然,原命题成立。

(02) 当h>0,且树的高度为 h-1 时,它包含的节点个数至少为 2bh(x)-1-1。这个是根据(01)推断出来的!

    下面,由树的高度为 h-1 的已知条件推出“树的高度为 h 时,它所包含的节点树为 2bh(x)-1”。

    当树的高度为 h 时,
    对于节点x(x为根节点),其黑高度为bh(x)。
    对于节点x的左右子树,它们黑高度为 bh(x) 或者 bh(x)-1。
    根据(02)的已知条件,我们已知 "x的左右子树,即高度为 h-1 的节点,它包含的节点至少为 2bh(x)-1-1 个";

    所以,节点x所包含的节点至少为 ( 2bh(x)-1-1 ) + ( 2bh(x)-1-1 ) + 1 = 2^bh(x)-1。即节点x所包含的节点至少为 2bh(x)-1。
    因此,原命题成立。

    由(01)、(02)得出,"高度为h的红黑树,它的包含的内节点个数至少为 2^bh(x)-1个"。
    因此,“一棵含有n个节点的红黑树的高度至多为2log(n+1)”。

三、树的旋转知识

节点的数据结构:

package com.red_black;
/**
* @author 作者 : 李
* @data 创建时间 : 2017年7月20日 上午9:27:39
* @version 版本 : 1.0
* @function 功能 : 红黑树的节点的数据结构
*/
public class NodeStruct {
	private boolean color;
	private float nodeValue;
	private NodeStruct parentNode;
	private NodeStruct leftNode;
private NodeStruct rightNode;

public NodeStruct() {
	// TODO Auto-generated constructor stub
}
/**
 * @param color
 * @param nodeValue
 * @param parentNode
 * @param leftNode
 * @param rightNode
 */
public NodeStruct(boolean color, float nodeValue, NodeStruct parentNode, NodeStruct leftNode,
		NodeStruct rightNode) {
	super();
	this.color = color;
	this.nodeValue = nodeValue;
	this.parentNode = parentNode;
	this.leftNode = leftNode;
	this.rightNode = rightNode;
	}
}
//省略个get和set函数... 
    

当在对红黑树进行插入和删除等操作时,对树做了修改可能会破坏红黑树的性质。为了继续保持红黑树的性质,可以通过对结点进行重新着色,以及对树进行相关的旋转操作,即通过修改树中某些结点的颜色及指针结构,来达到对红黑树进行插入或删除结点等操作后继续保持它的性质或平衡的目的。

    树的旋转分为左旋和右旋,下面借助图来介绍一下左旋和右旋这两种操作。

1.左旋


如上图所示,当在某个结点pivot上,做左旋操作时,我们假设它的右孩子y不是NIL[T],pivot可以为任何不是NIL[T]的左子结点。左旋以pivot到Y之间的链为“支轴”进行,它使Y成为该子树的新根,而Y的左孩子b则成为pivot的右孩子。

左旋的Java代码:

/**
*//所在包com.red_black.CommonUtil  
* @param x
*            对某一node节点进行左旋
*/
public static NodeStruct leftRotate(NodeStruct root, NodeStruct x) {
	// 将y节点设置为旋转节点x的右子节点
	NodeStruct y = x.getRightNode();
		// 将x的右子节点设置为y的左子节点
		x.setRightNode(y.getLeftNode());
		// 将y的左子节点的父节点设置为x节点
		y.getLeftNode().setParentNode(x);
		//如果x的父节点为根节点,则将y设置为根节点
		if (x.getParentNode() == null)
		{
			root = y;
		}
		else
		{
			//如果x是它父节点的左子节点,则将x的父节点的左子节点设置为y节点,否则将x的父节点的右子节点设置为y节点
			if(x.getParentNode().getLeftNode() == x)
			{
				x.getParentNode().setLeftNode(y);
			}else
			{
				x.getParentNode().setRightNode(y);
			}
		}
		//将x的父亲设置为y节点的父亲
		y.setParentNode(x.getParentNode());
		//将y的左子节点设置为x节点
		y.setLeftNode(x);
		//将x的父亲设置为y节点
		x.setParentNode(y);
		//返回插入节点node并左旋后的红黑树
		return root;
}

2.右旋

右旋与左旋差不多,再此不做详细介绍。


树在经过左旋右旋之后,树的搜索性质保持不变树的红黑性质则被破坏了所以,红黑树插入和删除数据后,需要利用旋转颜色重涂来重新恢复树的红黑性质

右旋的Java代码:

/**
 * //所在包com.red_black.CommonUtil 
 * @param x
 *            对某一node节点进行右旋
 */
public static NodeStruct rightRotate(NodeStruct root, NodeStruct x) {
	// 将y节点设置为旋转节点x的左子节点
	NodeStruct y = x.getLeftNode();
	// 将x的左子节点设置为y的右子节点
	x.setLeftNode(y.getRightNode());
	// 将y的右子节点的父节点设置为x节点
	y.getRightNode().setParentNode(x);
	//如果x的父节点为根节点,则将y设置为根节点
	if (x.getParentNode() == null)
	{
		root = y;
		}
		else
		{
			//如果x它父节点的左子节点,则将x的父节点的左子节点设置为y节点,否则将x的父节点的右子节点设置为y节点
			if(x.getParentNode().getLeftNode() == x)
			{
				x.getParentNode().setLeftNode(y);
			}else
			{
				x.getParentNode().setRightNode(y);
			}
		}
		//将x的父亲设置为y节点的父亲
		y.setParentNode(x.getParentNode());
		//将y的右子节点设置为x节点
		y.setRightNode(x);
		//将x的父亲设置为y节点
		x.setParentNode(y);
		//返回插入节点node并右旋后的红黑树
		return root;
}

四、红黑树的插入

要真正理解红黑树的插入,还得先理解二叉查找树的插入。磨刀不误砍柴工,咱们再来了解下二叉查找树的插入和红黑树的插入

如果要在二叉查找树中插入一个结点,首先要查找到结点插入的位置,然后进行插入假设插入的结点为z的话,插入的Java代码如下:

/**
 * //所在包com.red_black.CommonUtil
 * @param node 插入的节点
 */
public static insertNode(NodeStruct root, NodeStruct node)
{
	//插入点node的父节点
	NodeStruct nodeParent = null;
	//红黑树的根节点
	NodeStruct indexNode = root;
	//查找插入的点node的父节点nodeParent
	while(indexNode != null)
	{
		nodeParent = indexNode;
		if(node.getNodeValue() < indexNode.getNodeValue())
		{
			indexNode = indexNode.getLeftNode();
		}
		else
		{
			indexNode = indexNode.getRightNode();
		}
	}
	//将插入点的父节点设置为nodeParent
	node.setParentNode(nodeParent);
	
	//如果nodeParent为null,则将插入点node是根节点
	if(nodeParent == null)
	{
		root = node;
	}else if(node.getNodeValue() < nodeParent.getNodeValue())
	{
		//将插入点node设置为它父节点nodeParent的左子节点
		nodeParent.setLeftNode(node);
	}else
	{
		//将插入点node设置为它父节点nodeParent的右子节点
		nodeParent.setRightNode(node);
	}
	//返回插入节点node后的红黑树
	return root;
}

红黑树的插入和插入修复

现在我们了解了二叉查找树的插入,接下来,咱们便来具体了解红黑树的插入操作。红黑树的插入相当于在二叉查找树插入的基础上,为了重新恢复平衡,继续做了插入修复操作。

       假设插入的结点为z,红黑树的插入java代码具体如下所示:

/**
 * //所在包com.red_black.CommonUtil
 * @param node 插入的节点
 */
public static insertNode(NodeStruct root, NodeStruct node)
{
	//插入点node的父节点
	NodeStruct nodeParent = null;
	//红黑树的根节点
	NodeStruct indexNode = root;
	//查找插入的点node的父节点nodeParent
	while(indexNode != null)
	{
		nodeParent = indexNode;
		if(node.getNodeValue() < indexNode.getNodeValue())
		{
			indexNode = indexNode.getLeftNode();
		}
		else
		{
			indexNode = indexNode.getRightNode();
		}
	}
	//将插入点的父节点设置为nodeParent
	node.setParentNode(nodeParent);
	
	//如果nodeParent为null,则将插入点node是根节点
	if(nodeParent == null)
	{
		root = node;
	}else if(node.getNodeValue() < nodeParent.getNodeValue())
	{
		//将插入点node设置为它父节点nodeParent的左子节点
		nodeParent.setLeftNode(node);
	}else
	{
		//将插入点node设置为它父节点nodeParent的右子节点
		nodeParent.setRightNode(node);
	}
	//将插入点node节点的左右子节点设置为null,并将插入点设置为红色
	node.setLeftNode(null);
	node.setRightNode(null);
	node.setColor(RED);
	//对插入点node进行修复,使它继续满足红黑树的五个性质
	root = RB_Insert_FixUp(root, node);
	//返回插入节点node后的红黑树
	return root;
 }

         把上面这段红黑树的插入代码,跟之前看到的二叉查找树的插入代码比较一下可以看出,红黑树的插入代码多了几行红色标注的代码,最后为保证红黑性质在插入操作后依然保持,调用一个辅助程序RB_Insert_FixUp来对结点进行重新着色,并旋转。

         换言之,如果插入的是根结点,由于原树是空树,此情况只会违反性质2因此直接把此结点涂为黑色;如果插入的结点的父结点是黑色,由于此不会违反性质2和性质4,红黑树没有被破坏,所以此时什么也不做。

         但当遇到下述3种情况时又该如何调整呢?

             ● 插入修复情况1:如果当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)是红色

             ● 插入修复情况2:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子

             ● 插入修复情况3:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左子

       答案就是根据红黑树插入代码RB-INSERT(T, z)最后一行调用的RB-INSERT-FIXUP(Tz)函数所示的步骤进行操作,具体如下所示:

/**
* //所在包com.red_black.CommonUtil
* 红黑树的调整和修复
* @param root
* @param node
* @return
*/
private static NodeStruct RB_Insert_FixUp(NodeStruct root, NodeStruct node) {
	// TODO Auto-generated method stub
	//判断插入点node的父节点是否为null
	while(node.getParentNode() != null && node.getParentNode().getColor()==RED )
	
		//如果node的父节点是node祖父节点的左子节点,则右旋
		if(node.getParentNode() == node.getParentNode().getParentNode().getLeftNode())
		{
			//node的叔叔节点
			NodeStruct nodeUncle = node.getParentNode().getParentNode().getRightNode();
			if(nodeUncle.getColor() == RED)
			{
				node.getParentNode().setColor(BLACK);
				nodeUncle.setColor(BLACK);
				node.getParentNode().getParentNode().setColor(RED);
				//设置当前节点
				node = node.getParentNode().getParentNode();
			}else if(node == node.getParentNode().getRightNode())
			{
				//设置当前节点的父节点
				node = node.getParentNode();
				//左旋
				root = leftRotate(root, node);
			}else if(node == node.getParentNode().getLeftNode())
			{
				//当前节点的父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋
				node.getParentNode().setColor(BLACK);
				node.getParentNode().getParentNode().setColor(RED);
				//右旋
				root = rightRotate(root, node.getParentNode().getParentNode());
				}
			}else
			{
				//node的叔叔节点
				NodeStruct nodeUncle = node.getParentNode().getParentNode().getLeftNode();
				if(nodeUncle.getColor() == RED)
				{
					node.getParentNode().setColor(BLACK);
					nodeUncle.setColor(BLACK);
					node.getParentNode().getParentNode().setColor(RED);
					//设置当前节点
					node = node.getParentNode().getParentNode();
				}else if(node == node.getParentNode().getLeftNode())
				{
					//设置当前节点
					node = node.getParentNode();
					//右旋
					root = rightRotate(root, node);
				}else if(node == node.getParentNode().getRightNode())
				{
					//当前节点的父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋
					node.getParentNode().setColor(BLACK);
					node.getParentNode().getParentNode().setColor(RED);
					//左旋
					root = leftRotate(root, node.getParentNode().getParentNode());
				}
			}
		}
		//插入节点node的父节点为null,则插入节点node设为根节点,颜色设置为黑色
		root.setColor(BLACK);
		//返回插入节点node并且修正后的红黑树
		return root;
	}


下面,咱们来分别处理上述3种插入修复情况

  • 插入修复情况1:当前结点的父结点是红色,祖父结点的另一个子结点(叔叔结点)是红色。

如下代码所示:

//如果node的父节点是node祖父节点的左子节点,则右旋
if(node.getParentNode() == node.getParentNode().getParentNode().getLeftNode())
{
	//node的叔叔节点
	NodeStruct nodeUncle = node.getParentNode().getParentNode().getRightNode();
        if(nodeUncle.getColor() == RED)

此时父结点的父结点一定存在,否则插入前就已不是红黑树。与此同时,又分为父结点是祖父结点的左孩子还是右孩子,根据对称性,我们只要解开一个方向就可以了。这里只考虑父结点为祖父左孩子的情况,如下图所示。

     

对此,我们的解决策略是:将当前节点的父节点和叔叔节点涂黑,祖父结点涂红,把当前结点指向祖父节点,从新的当前节点重新开始算法。即如下代码所示:

{
	node.getParentNode().setColor(BLACK);
	nodeUncle.setColor(BLACK);
	node.getParentNode().getParentNode().setColor(RED);
	//设置当前节点
	node = node.getParentNode().getParentNode();
}

所以,变化后如下图所示



于是,插入修复情况1转换成了插入修复情况2

  • 插入修复情况2:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的右子
此时,解决对策是:当前节点的父节点做为新的当前节点,以新当前节点为支点左旋。即如下代码所示:

else if(node == node.getParentNode().getRightNode())
{
	//设置当前节点的父节点
	node = node.getParentNode();
	//左旋
	root = leftRotate(root, node);
} 
所以红黑树由之前的:


变化成:


从而插入修复情况2转换成了插入修复情况3

  • 插入修复情况3:当前节点的父节点是红色,叔叔节点是黑色,当前节点是其父节点的左孩子
         解决对策是:父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋,操作代码为:

else if(node == node.getParentNode().getLeftNode())
{
	//当前节点的父节点变为黑色,祖父节点变为红色,在祖父节点为支点右旋
	node.getParentNode().setColor(BLACK);
	node.getParentNode().getParentNode().setColor(RED);
	//右旋
	root = rightRotate(root, node.getParentNode().getParentNode());
}
最后,把根结点涂为黑色,整棵红黑树便重新恢复了平衡。所以红黑树由之前的:



变化成:


//主函数
package com.red_black;
/**
* @author 作者 : 李
* @data 创建时间 : 2017年7月20日 上午9:10:50
* @version 版本 : 1.0
* @function 功能 : 红黑树的算法实现
*/
public class RedAndBlack {
	private static NodeStruct RBTreeBoot = null;
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		//插入节点
		float[] nodeValue = {11, 2, 14, 1, 7, 15, 5, 8, 4, 13};
		for(int i = 0; i < nodeValue.length; i++)
		{
			NodeStruct node = new NodeStruct();
			node.setNodeValue(nodeValue[i]);
			RBTreeBoot = CommonUtil.insertNode(RBTreeBoot, node);
		}
		
		if(RBTreeBoot !=null)
		{
			//深度优先遍历或者前序遍历的递归展示二叉树
			RedAndBlack.show(RBTreeBoot);
		}else
		{
			System.out.println("一颗空的红黑树!");
		}
		
	}
	
	public static void show(NodeStruct tree)
	{
		if(tree.getLeftNode() != null || tree.getRightNode() !=null)
		{
			if(tree.getLeftNode() != null)
			{
				float x = tree.getLeftNode().getNodeValue();
				boolean color = tree.getLeftNode().getColor();
				String colorStr = null;
				if(color)
				{
					colorStr = "RED";
				}else
				{
					colorStr = "BLACK";
				}
				String parentColor = tree.getColor() ? "RED" : "BLACK";
				System.out.println("left: " + "color: " + colorStr + "-->" + x + "  --父节点-->  " + parentColor + "  " + tree.getNodeValue());
				show(tree.getLeftNode());
			}
			if(tree.getRightNode() != null)
			{
				float y = tree.getRightNode().getNodeValue();
				boolean color = tree.getRightNode().getColor();
				String colorStr = null;
				if(color)
				{
					colorStr = "RED";
				}else
				{
					colorStr = "BLACK";
				}
				String parentColor = tree.getColor() ? "RED" : "BLACK";
				System.out.println("right: " + "color: " + colorStr + "-->" + y + "  --父节点-->  " + parentColor + "  " + tree.getNodeValue());
				show(tree.getRightNode());
			}
		}else
		{
			if(tree.getParentNode() == null)
			{
				System.out.println("该红黑树仅有根节点,且  “"+tree.getNodeValue()+ "” 为红黑树的根节点");
			}
		}
	}
}



 

猜你喜欢

转载自blog.csdn.net/li2327234939/article/details/75628448