Red Black Tree(红黑树)

Posted 凉茶方便面

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Red Black Tree(红黑树)相关的知识,希望对你有一定的参考价值。

当二叉平衡树在树高较高时,许多操作的代价较高,甚至比不过链表,此时可以采用红黑树。红黑树是许多平衡搜索树中的一种,可以保证在最坏情况下基本动态集合操作的时间复杂度为O(lg n)。红黑树有很多的应用,其中比较多的应用是各种类库中的Map,在Java中的TreeMap就是使用红黑树构建的,C++中的STL带的Map的结构也是红黑树构建的。

红黑树的性质

红黑树是一种二叉搜索树,它在每个节点上增加一个存储节点颜色信息的位,可以是RED或BLACK。由于颜色的约束,红黑树保证没有一条路径比其他路径长2倍,因此近似于平衡的。红黑树中每个结点包含5个属性:color,key,left,right和p。

private static enum Color
	RED,
	BLACK;

private static class Node
	int key;
	Color color;
	Node left, right, p;
	public Node(int key)
		this.key = key;
		this.color = Color.BLACK;
	

private final Node nil = new Node(Integer.MIN_VALUE);

红黑树是一种满足如下性质的二叉搜索树:

  1. 每个结点或是红色的,或是黑色的;
  2. 根节点是黑色的;
  3. 每个叶节点(nil)都是黑色的;
  4. 如果一个结点是红色的,则它的两个子节点都是黑色的;
  5. 对每个结点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色结点。

定义从某个结点x(不含该结点)出发到达一个叶节点的任意一条简单路径上的黑色结点个数为该结点的黑高(black height),即为bh(x)。那么从该结点出发的所有下降到其叶节点的简单路径的黑结点个数都相同。

旋转

为了在插入和删除操作过程中维护红黑树的性质,必须在操作树时,对树中的某些结点的颜色以及指针结构进行修改。其中指针结构的修改一般是通过旋转来完成的。

LeftRotate和RightRotate都在O(1)时间内运行完成,在旋转操作中只有指针改变,其他属性都保持不变。

private void LeftRotate(Node x) 
	RBTree T = this;
	Node y = x.right;
	x.right = y.left;
	if (y.left != T.nil) 
		y.left.p = x;
	
	y.p = x.p;
	if (x.p == T.nil)
		T.root = y;
	else if (x == x.p.left)
		x.p.left = y;
	else
		x.p.right = y;
	y.left = x;
	x.p = y;

private void RightRotate(Node x) 
	RBTree T = this;
	Node y = x.left;
	x.left = y.right;
	if (y.right != T.nil) 
		y.right.p = x;
	
	y.p = x.p;
	if (x.p == T.nil)
		T.root = y;
	else if (x == x.p.right)
		x.p.right = y;
	else
		x.p.left = y;
	y.right = x;
	x.p = y;

插入

我们可以在O(lg n)的时间内完成向一棵含有n个点的红黑树插入新节点,当插入时可能会导致整棵树违背红黑树的性质,因此在插入后需要使用RBInsertFixup对结点进行重新着色并旋转。

public void RBInsert(Node z) 
	RBTree T = this;
	Node y = T.nil, x = T.root;
	while (x != T.nil) 
		y = x;
		if (z.key < x.key)
			x = x.left;
		else
			x = x.right;
	
	z.p = y;
	if (y == T.nil)
		T.root = z;
	else if (z.key < y.key)
		y.left = z;
	else
		y.right = z;
	z.left = T.nil;
	z.right = T.nil;
	z.color = Color.RED;
	RBInsertFixup(z);

这个插入算法是根据二叉搜索树的算法改变过来的(完整内容可以参看博客:Binary Search Tree),其基本思想还是根据要插入的值的大小,按照二叉搜索树的插入原则(即,保证每个结点左边的元素都比它的父节点小,右边元素都比父节点大),找到合适的插入位置,然后修改父子结点的指针。但是,由于插入时结点需要带上颜色(新加入的结点颜色需要着色为红色),同时由于添加时会造成红黑树的性质发生变化(黑高虽然没变,但是可能会造成红色结点是直接父子关系),所以需要使用RBInsertFixup对这棵子树进行调整。

//二叉搜索树的插入过程(对比以上的红黑树插入过程)
public void TreeInsert(BSTreeNode z) // this为BSTree
	BSTreeNode y = null, x = this.root;
	while (x != null) 
		y = x;
		if (z.key < x.key)
			x = x.left;
		else
			x = x.right;
	
	z.p = y;
	if (y == null)
		this.root = z;// tree是空的
	else if (z.key < y.key)
		y.left = z;
	else
		y.right = z;

从以上代码中可以看到相对于TreeInsert 来说RBInsert多做了三件事情:使用内部的nil对象替换了TreeInsert中所出现的null;将新加入的结点设置为红色并设置其左右子节点为nil对象;使用额外的RBInsertFixup维护整棵红黑树的特性(以下会重点介绍RBInsertFixup是如何维护红黑树的特性的)。

private void RBInsertFixup(Node z) 
	RBTree T = this;
	while (z.p.color == Color.RED) 
		if (z.p == z.p.p.left) // z在左子树上
			Node y = z.p.p.right;
			if (y.color == Color.RED) // 情况1
				z.p.color = Color.BLACK;
				y.color = Color.BLACK;
				z.p.p.color = Color.RED;
				z = z.p.p;
			 else 
				if (z == z.p.right) // 情况2
					z = z.p;
					LeftRotate(z);
				
				z.p.color = Color.BLACK;// 情况3
				z.p.p.color = Color.RED;
				RightRotate(z.p.p);
			
		 else 
			// 和上面的情况刚好对称
			Node y = z.p.p.left;
			if (y.color == Color.RED) // 情况1
				z.p.color = Color.BLACK;
				y.color = Color.BLACK;
				z.p.p.color = Color.RED;
				z = z.p.p;
			 else 
				if (z == z.p.left) // 情况2
					z = z.p;
					RightRotate(z);
				
				z.p.color = Color.BLACK;// 情况3
				z.p.p.color = Color.RED;
				LeftRotate(z.p.p);
			
		
	
	T.root.color = Color.BLACK;// 修复根节点

根据插入节点z被设定为红色来看,可能破坏掉红黑树的特性有性质2和性质4,即根节点为黑色(树为空时添加了一个元素)以及红色结点不能有红色孩子。也就是说,插入后z结点可能是新插入的红色的root或者z结点的父节点为红色。

那么不考虑z插入后为root的情况(以后单独考虑这种情况),在插入节点z后,如果z的父节点为黑色,那么是不会造成红黑树的性质变化的,但是如果z的父节点时红色,红黑树则有以下三种情况需要分析:

  1. z的叔结点y为红色(虽然z可能是左子树也可能是右子树,但是可以一起考虑);
  2. z的叔结点y是黑色且z是一个右孩子;
  3. z的叔结点y是黑色且z是一个左孩子。

以上三种情况我们没有分析z是新添加的root的情况:此时z为root,且被着色为红色,其左右子节点都是黑色,违背了性质2,故需要我们在代码中直接设置root为黑色。在此之后我们就可以重点考虑剩下的三种情况了(考虑以下情况时,要对比上图才容易理解)。

情况1:z的叔结点y为红色

如果z的叔结点y是红色,那么根据红黑树的性质——红色结点不可能是相邻的,可以得出z.p.p的颜色为黑色,又考虑到我们的前提是z.p为红色,我们可以得出:z,z.p,z的叔结点y都为红色,但是z.p.p为黑色。这种情况下,我们可以把z.p.p的黑色取消掉并分给它的两个孩子(z.p和z的叔结点y),这样可以保证z.p.p这棵子树的黑高不变,且其左右子树的黑高相同。


情况2:z的叔结点y是黑色的且z是一个右孩子

情况3:z的叔结点y是红色的且z是一个左孩子

情况2是可以转化为情况3的,我们可以通过一次旋转,将情况2转化为情况3:如z是父节点的右子树,那么左旋转z结点:z变为z.p的父节点,z.p就变成z的左子树,此时把原来的z.p当做新的z’,最后我们会看到z’为z’.p的左子树。

对于情况3,因为我们需要保证这棵子树的黑高不变,又不能让它的左右子树的黑高失衡,故我们需要在z’.p上进行一次右旋,然后将z’.p.p设置为红色,将z’.p设置为黑色。这样就可以保证黑高不变,且左右子树的黑高也不会有所变化。


这个插入工作的核心都在查找合适的插入位置并维护红黑树特性,在二叉平衡树中寻找合适的插入位置复杂度为O(lg n),而维护红黑树的特性大致也需要O(lg n)的时间,故RBInsert总花费为O(lg n)。

删除

红黑树的删除操作和二叉搜索树类似,但是由于将来需要维护红黑树的特性,故需要记录一些可能造成红黑树性质失效的结点的踪迹。假如我们要删除节点z:如果z拥有少于2个子节点时,直接删除z,并让z的子节点y成为z;假如z有两个子节点时,y是z的(中序遍历)后继,将y移至z的位置,结点移除或者移动之前必须记录y的颜色,并且记录y的右子树的踪迹,将x移至树中y原来的位置(至于为什么一定是记录y的右子树,原因是这样的:y是z的后继,那y就是z的右子树最小元素,它没有左子树)。

public void RBDelete(Node z) 
	RBTree T = this;
	Node y = z, x = null;
	Color yOriginalColor = y.color;
	if (z.left == T.nil) // 情况1
		x = z.right;
		RBTransplant(z, z.right);
	 else if (z.right == T.nil) 
		x = z.left;
		RBTransplant(z, z.left);
	 else // 情况2
		y = TreeMininum(z.right);//情况2.1
		yOriginalColor = y.color;
		x = y.right;
		if (y.p == z) 
			x.p = y;
		 else //情况2.2
			RBTransplant(y, y.right);
			y.right = z.right;
			y.right.p = y;
		
		RBTransplant(z, y);//情况2.3
		y.left = z.left;
		y.left.p = y;
		y.color = z.color;
	
	if (yOriginalColor == Color.BLACK) 
		RBDeleteFixup(x);
	


从图中可以看到有两种情况:

  1. 只有一个子节点时,直接用子节点来替换要删除的结点;
  2. 有两个子节点时,考虑情况2.1,它会寻找z的右子树中它的直接后继,也就是右子树中的最小值y,这个最小值y在右子树的左下方,y没有左子树,这时用这个最小值y替换z的位置,同时用最小值y的右子树x替换这个最小值y的原来的位置,这样就把情况2.1变成情况2.2了。我们可以看到情况2.2和情况2.3是同样的结构的,所以可以用同样的代码,在情况2.3中可以看到,结点y是没有左子树的,所以可以用y直接向上替换z。
虽然上述步骤能够把结点z删除,但是删除后还需要额外维护红黑树的特性,故需要使用RBDeleteFixup方法去维护删除后红黑树的特性。

现在考虑何时需要维护红黑树的特性,我们看到,删除时如果造成y所在的子树的黑高减少,此时就需要维护红黑树的特性了。就如图中情况2.1中的y向上替换z,导致r的左子树黑高减去1,;情况2.3中,y替换了z,导致y所在的这个子树的总黑高减去了1。

造成y所在子树的黑高变化的原因是y本身是黑色的,因为如果y是红色,当y被删除或移动时,红黑树性质不变,因为:

  1. 树中的黑高没有变化;
  2. 不存在两个相邻的红色结点。因为y在树中占据了z的位置,再考虑到z的颜色,树中y的新位置不可能同时有相邻的两个红色结点。另外,如果y不是z的右孩子,则y的原右孩子x替代y。如果y是红色,则x一定是黑色,因此用x替代y不可能使两个红色结点相邻;
  3. 如果y是红色,就不可能是根节点,所以根节点仍然是黑色。

当y是黑色结点时,会造成如下影响:

  1. 如果y是原来的根结点(即是说,要删除的结点是root,因为这种情况下y和z相同),则y的一个红色孩子成为新的根节点;
  2. 如果x和x.p是红色(就像情况2.2);
  3. 树中移动y导致先前任何包含y的简单路径上的黑色结点数量减1(就像情况2.1)。

为了解决问题1我们需要设置不管如何改变都让root为BLACK;为了解决问题2和问题3,我们会假设将y的黑色留给了x,这样x就拥有了除了本身的颜色外,还有多余的一个黑色,然后我们设法通过RBDeleteFixup把这层黑色分给别的结点。

private void RBDeleteFixup(Node x) 
	RBTree T = this;
	while (x != T.root && x.color == Color.BLACK) 
		if (x == x.p.left) 
			Node w = x.p.right;

			if (w.color == Color.RED) // 情况1
				w.color = Color.BLACK;
				x.p.color = Color.RED;
				LeftRotate(x.p);
				w = x.p.right;
			
			if (w.left.color == Color.BLACK && w.right.color == Color.BLACK) // 情况2
				w.color = Color.RED;
				x = x.p;
			 else 
				if (w.right.color == Color.BLACK) // 情况3
					w.left.color = Color.BLACK;
					w.color = Color.RED;
					RightRotate(w);
					w = x.p.right;
				
				w.color = x.p.color;// 情况4
				x.p.color = Color.BLACK;
				w.right.color = Color.BLACK;
				LeftRotate(x.p);
				x = root;
			
		 else  // symmetric
			Node w = x.p.left;

			if (w.color == Color.RED) // 情况1
				w.color = Color.BLACK;
				x.p.color = Color.RED;
				RightRotate(x.p);
				w = x.p.left;
			
			if (w.right.color == Color.BLACK && w.left.color == Color.BLACK) // 情况2
				w.color = Color.RED;
				x = x.p;
			 else 
				if (w.left.color == Color.BLACK) // 情况3
					w.right.color = Color.BLACK;
					w.color = Color.RED;
					LeftRotate(w);
					w = x.p.left;
				
				w.color = x.p.color;// 情况4
				x.p.color = Color.BLACK;
				w.left.color = Color.BLACK;
				RightRotate(x.p);
				x = root;
			
		
	
	x.color = Color.BLACK;


调整时分为四种情况(逻辑上认为x除了本身颜色外还有一层黑色):

1.X的兄弟节点w是红色;

W是红色结点,它拥有黑色子节点,所以可以交换w和x.p的颜色,然后对x.p做一次左旋而不违反红黑树的任何性质。这样经过一次左旋,就把情况1转化为情况2,3,4了。

2.X的兄弟节点w是黑色,且w的两个子节点是黑色;

因为w是黑色,所以从x和w上去掉一层黑色,使得x只有一重黑色w为红色,为了使得x.p这棵子树的黑高不变,需要在x.p上添加一层虚拟的黑色,并把x.p作为新的x。如果是从情况1到情况2的,那么由于x.p原本是红色,情况2调整后会将x.p设置为黑色,这样算法的循环就会结束。

3.X的兄弟节点w是黑色,且w的左孩子是红色,右孩子是黑色;

交换w和左孩子w.left的颜色,然后对w进行右旋而不违反红黑树的性质,这样就可以把它转化为情况4.

4.X的兄弟节点w是黑色,且w的右孩子是红色;

这种情况下,结点x的兄弟节点w为黑色且w的右孩子为红色,通过进行某些颜色修改并对x.p做一次左旋,可以去掉x的额外黑色,从而使它变为单重黑色,而且不破坏红黑树的任何性质。从图中看出,相当于将D的颜色给E,将A的多余黑色给B,然后使用D作为新的根。

从以上四种情况可以看出,基本上就是希望x和它的兄弟节点都是黑色,然后希望x的兄弟节点的右孩子是红色,这样x就能把它多余的颜色交给它父亲,w就能把它多余的颜色交给右孩子,然后进行树的调整就能将x的多余颜色分给别的结点而不改变红黑树的特性。

进行删除时,查找要删除的元素需要的时间是O(lg n),而进行调整时会进行有限次的旋转和颜色处理,故RBDeleteFixup的时间复杂度是O(lg n)。

源代码

附红黑树完整代码(可以用先序和后序的遍历序列重建出树的结构进行验证算法):

public class RBTree 
	private static enum Color 
		RED, BLACK;
	

	public static class Node 
		int key;
		Color color;
		Node left, right, p;

		public Node(int key) 
			this.key = key;
			// 将颜色默认为黑色,但是在程序中显式设置为红色
			this.color = Color.BLACK;
		
	

	private final Node nil = new Node(Integer.MIN_VALUE);
	private Node root = null;

	public RBTree() 
		root = nil;
	

	// 插入
	public void RBInsert(Node z) 
		RBTree T = this;
		Node y = T.nil, x = T.root;
		while (x != T.nil) 
			y = x;
			if (z.key < x.key)
				x = x.left;
			else
				x = x.right;
		
		z.p = y;
		if (y == T.nil)
			T.root = z;
		else if (z.key < y.key)
			y.left = z;
		else
			y.right = z;
		z.left = T.nil;
		z.right = T.nil;
		z.color = Color.RED;
		RBInsertFixup(z);
	

	private void RBInsertFixup(Node z) 
		RBTree T = this;
		while (z.p.color == Color.RED) 
			if (z.p == z.p.p.left) // z在左子树上
				Node y = z.p.p.right;
				if (y.color == Color.RED) // 情况1
					z.p.color = Color.BLACK;
					y.color = Color.BLACK;
					z.p.p.color = Color.RED;
					z = z.p.p;
				 else 
					if (z == z.p.right) // 情况2
						z = z.p;
						LeftRotate(z);
					
					z.p.color = Color.BLACK;// 情况3
					z.p.p.color = Color.RED;
					RightRotate(z.p.p);
				
			 else 
				// 和上面的情况刚好对称
				Node y = z.p.p.left;
				if (y.color == Color.RED) // 情况1
					z.p.color = Color.BLACK;
					y.color = Color.BLACK;
					z.p.p.color = Color.RED;
					z = z.p.p;
				 else 
					if (z == z.p.left) // 情况2
						z = z.p;
						RightRotate(z);
					
					z.p.color = Color.BLACK;// 情况3
					z.p.p.color = Color.RED;
					LeftRotate(z.p.p);
				
			
		
		T.root.color = Color.BLACK;
	

	private void LeftRotate(Node x) 
		RBTree T = this;
		Node y = x.right;
		x.right = y.left;
		if (y.left != T.nil) 
			y.left.p = x;
		
		y.p = x.p;
		if (x.p == T.nil)
			T.root = y;
		else if (x == x.p.left)
			x.p.left = y;
		else
			x.p.right = y;
		y.left = x;
		x.p = y;
	

	private void RightRotate(Node x) 
		RBTree T = this;
		Node y = x.left;
		x.left = y.right;
		if (y.right != T.nil) 
			y.right.p = x;
		
		y.p = x.p;
		if (x.p == T.nil)
			T.root = y;
		else if (x == x.p.right)
			x.p.right = y;
		else
			x.p.left = y;
		y.right = x;
		x.p = y;
	

	// 删除
	private void RBTransplant(Node u, Node v) // 用v替换u
		RBTree T = this;
		if (u.p == null || u.p == T.nil)
			T.root = v;
		else if (u == u.p.left)
			u.p.left = v;
		else
			u.p.right = v;

		if (v != null)
			v.p = u.p;
	

	public Node TreeMininum(Node x) 
		while (x != null && x.left != nil)
			x = x.left;
		return x;
	

	public void RBDelete(Node z) 
		RBTree T = this;
		Node y = z, x = null;
		Color yOriginalColor = y.color;
		if (z.left == T.nil) // 情况1
			x = z.right;
			RBTransplant(z, z.right);
		 else if (z.right == T.nil) 
			x = z.left;
			RBTransplant(z, z.left);
		 else // 情况2
			y = TreeMininum(z.right);//情况2.1
			yOriginalColor = y.color;
			x = y.right;
			if (y.p == z) 
				x.p = y;
			 else //情况2.2
				RBTransplant(y, y.right);
				y.right = z.right;
				y.right.p = y;
			
			RBTransplant(z, y);//情况2.3
			y.left = z.left;
			y.left.p = y;
			y.color = z.color;
		
		if (yOriginalColor == Color.BLACK) 
			RBDeleteFixup(x);
		
	

	private void RBDeleteFixup(Node x) 
		RBTree T = this;
		while (x != T.root && x.color == Color.BLACK) 
			if (x == x.p.left) 
				Node w = x.p.right;

				if (w.color == Color.RED) // 情况1
					w.color = Color.BLACK;
					x.p.color = Color.RED;
					LeftRotate(x.p);
					w = x.p.right;
				
				if (w.left.color == Color.BLACK && w.right.color == Color.BLACK) // 情况2
					w.color = Color.RED;
					x = x.p;
				 else 
					if (w.right.color == Color.BLACK) // 情况3
						w.left.color = Color.BLACK;
						w.color = Color.RED;
						RightRotate(w);
						w = x.p.right;
					
					w.color = x.p.color;// 情况4
					x.p.color = Color.BLACK;
					w.right.color = Color.BLACK;
					LeftRotate(x.p);
					x = root;
				
			 else  // symmetric
				Node w = x.p.left;

				if (w.color == Color.RED) // 情况1
					w.color = Color.BLACK;
					x.p.color = Color.RED;
					RightRotate(x.p);
					w = x.p.left;
				
				if (w.right.color == Color.BLACK && w.left.color == Color.BLACK) // 情况2
					w.color = Color.RED;
					x = x.p;
				 else 
					if (w.left.color == Color.BLACK) // 情况3
						w.right.color = Color.BLACK;
						w.color = Color.RED;
						LeftRotate(w);
						w = x.p.left;
					
					w.color = x.p.color;// 情况4
					x.p.color = Color.BLACK;
					w.left.color = Color.BLACK;
					RightRotate(x.p);
					x = root;
				
			
		
		x.color = Color.BLACK;
	

	public void print() 
		RBTree T = this;
		System.out.print("先序 ");
		dfs(T, T.root);
		System.out.print(" 中序");
		dfs2(T, T.root);
		System.out.println();
	

	private void dfs(RBTree T, Node z) 
		if (z == T.nil)
			return;

		if (z.color == Color.BLACK)
			System.out.print("(" + z.key + "),");
		else
			System.out.print(z.key+",");
		dfs(T, z.left);
		dfs(T, z.right);
	
	private void dfs2(RBTree T, Node z) 
		if (z == T.nil)
			return;
		
		dfs2(T, z.left);
		if (z.color == Color.BLACK)
			System.out.print("(" + z.key + "),");
		else
			System.out.print(z.key+",");
		dfs2(T, z.right);
	

	public static void main(String[] args) 
		RBTree T = new RBTree();

		int values[] =  11, 2, 14, 1, 7, 15, 5, 8 ;
		Node n[] = new Node[values.length];
		for (int i = 0; i < values.length; i++) 
			Node z = new Node(values[i]);
			n[i] = z;
			T.RBInsert(z);
		
		for (int i = 0; i < values.length; i++) 
			T.print();
			System.out.println("-------------");
			T.RBDelete(n[i]);
		
		T.print();
	


以上是关于Red Black Tree(红黑树)的主要内容,如果未能解决你的问题,请参考以下文章

红黑树(Red-Black Tree)图文解析

手撸红黑树-Red-Black Tree 入门

红黑树(Red Black Tree)

红黑树(Red-Black Tree)图文解析

5分钟学会红黑树插( insertint elements into a red black tree)

手撕红黑树(Red-Black Tree)