红黑树原理和实现(2)

二 红黑树(Red-Black Tree)

       在上一篇博客中已经比较完整地介绍了BST(Binary Search Tree)的基本性质和各种操作的代码实现,对BST有较深刻的理解后再理解RBT(Red-Black Tree)就不会很吃力了。

       首先简单了解一下什么是RBT,来自百度百科:红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由Rudolf Bayer发明的,他称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的:它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。

       从上述可以看出,RBT虽然复杂,但是其操作是高效的。由于它本身是一种自平衡BST,所以它具有BST的所有性质。另外,RBT有自己特殊的性质,摘自《Introduction To Algorithms》:

       <1>Every node is either red or black.
       <2>The root is black.
       <3>Every leaf (NIL) is black.
       <4>If a node is red, then both its children are black.
       <5>For each node, all simple paths from the node to descendant leaves contain the same number of black nodes.

       从上述性质可以初步看出RBT跟BST最大的不同就是它多了一个颜色域,而这个颜色域非红即黑。

       RBT的示意图如图5所示,来自《Introduction To Algorithms》插图:


图5 RBT示意图

       其中黑色表示颜色为“Black”,灰色表示颜色为“Red”,T.nil表示的是sentinel(哨兵)结点,它的作用类似于BST中的NULL,目的是为了便于处理边界和节省空间,它表示所有的叶子结点和根结点的父结点。

       首先给出头文件rbt.h:

#ifndef __RBT_H__
#define __RBT_H__

typedef enum __rbt_color {
	RED,
	BLACK
} rbt_color;

typedef struct __rbt_node {
	int key;
	struct __rbt_node *parent;
	struct __rbt_node *left;
	struct __rbt_node *right;
	rbt_color color;
} rbt_node;

rbt_node *rbt_minimum(rbt_node *root);
rbt_node *rbt_maximun(rbt_node *root);
rbt_node *rbt_search(rbt_node *root, int key);
rbt_node *rbt_transplant(rbt_node **root, rbt_node *u, rbt_node *v);
void rbt_left_rotate(rbt_node **root, rbt_node *node);
void rbt_right_rotate(rbt_node **root, rbt_node *node);
int rbt_insert(rbt_node **root, int key);
void rbt_delete(rbt_node **root, rbt_node *del);
void rbt_preorder_walk(rbt_node *root);
void rbt_inorder_walk(rbt_node *root);
void rbt_postorder_walk(rbt_node *root);

#endif

以及sentinel的定义:

static rbt_node sentinel = {
	0,		// key
	&sentinel,	// parent
	&sentinel,	// left
	&sentinel,	// right
	BLACK	// color
};
2.1 旋转

       RBT的插入和删除除了跟BST有相同的原理之外,还有额外的修正(fixup)操作,因为插入和删除操作可能会破坏RBT的5个性质中的一些性质。而修正操作的核心是旋转,分为左旋和右旋。

2.1.1 左旋

       左旋和右旋示意图如图6所示,左旋是以x为支点,右旋以y为支点。

图6 左旋和右旋

       左旋操作包括的步骤如图7所示:

       <1>将支点(x)的right指针指向其右子结点(y)的左子结点,如果y的左子结点不为sentinel结点,那么将它的parent指针指向x。图7中红色虚线箭头所示。

       <2>将y的parent指针指向x的parent(分为x为其父结点的左子结点或者右子结点两种情况)。图7中蓝色虚线箭头所示。

       <3>将y的left指针指向x,x的parent指针指向y。图7中橙色虚线箭头所示。

图7 RBT左旋操作

       左旋操作的代码实现如下:

/*
 * 1st, deal with child pointer(sentinel or not)
 * 2nd, deal with parent pointer(sentinel or not)
 */
void rbt_left_rotate(rbt_node **root, rbt_node *node)
{
	rbt_node *y = node->right;

	node->right = y->left; // turn y's left subtree into x's right subtree
	if (y->left != &sentinel)
		y->left->parent = node; // y's left child adopted to node

	y->parent = node->parent; // link y's parent to node's parent

	if (node->parent == &sentinel)
		*root = y;
	else if (node == node->parent->left)
		node->parent->left = y;
	else
		node->parent->right = y;

	y->left = node;
	node->parent = y;
}

2.1.2 右旋

       右旋与左旋是对称的,所以只需要将rbt_left_rotate中的left修改为right即可,这里图示和代码实现略。

2.2 插入操作

       RBT的插入操作分为两个部分,第一部分跟BST的插入操作基本一样,第二部分是修正操作,因为插入结点有可能破坏RBT的性质。那么首先遇到的问题是插入的结点是什么颜色的呢?

       <1>考虑插入的结点是黑色的,那么它只会且肯定破坏性质5,因为插入结点是插入到叶子结点位置,从任一结点开始,任意向下到叶子结点的路径上黑色结点数不再相等,因为插入结点的这一路径上的黑色结点数多了一个。

       <2>考虑插入的结点是红色的,那么它可能破坏性质2或者4,因为当原树是空树的时候,插入一个红色结点就是根结点,而性质2指出它必须是黑色的;另外如果插入结点的父结点是红色的时候,性质4不满足。

       那么插入结点是用什么颜色呢?采用<1>方法时,每次都会破坏性质5,所以每次都会进行fixup操作,采用<2>方法时,可能会破坏性质2或者4,但是可以看到,修复性质2的操作很简单,将红色修改为黑色即可;修复性质4虽然与修复性质5的复杂度差不多,但是性质4是可能被破坏的,而不是一定被破坏。

       所以对比一下,插入结点时插入红色结点更好一些。插入结点操作的代码如下:

int rbt_insert(rbt_node **root, int key)
{
	rbt_node *ptr = &sentinel, *new = NULL;
	rbt_node *x = (*root == NULL) ? &sentinel : *root;
	
	new = (rbt_node *)malloc(sizeof(rbt_node));
	if (new == NULL) {
		printf("malloc error!\n");
		return -1;
	}

	// insert red node
	new->key = key;
	new->parent = &sentinel;
	new->left = &sentinel;
	new->right = &sentinel;
	new->color = RED;

	while (x != &sentinel) {
		ptr = x;
		if (new->key < x->key)
			x = x->left;
		else
			x = x->right;
	}

	new->parent = ptr;
	// root
	if (ptr == &sentinel)
		*root = new;
	else {
		if (new->key < ptr->key)
			ptr->left = new;
		else
			ptr->right = new;
	}

	rbt_insert_fixup(root, new);
	return 0;
}

       从代码中可以看出,大多数的操作跟BST的插入操作差不多,只是将BST的NULL修改为RBT的sentinel结点了,并且多了一个修正操作rbt_insert_fixup。从上述已经知道插入结点为红色的时候可能破坏RBT的性质2或者性质4,性质2被破坏很好解决,关键是性质4被破坏了要怎么解决。假设插入的结点为z,其父结点为z.p,当经过变换后,如果z和z.p之间的颜色不都是红色时,那么修正就完毕了。《Introduction To Algorithms》中采用的方法是修改z.p的颜色,将颜色冲突向根部移动,并且穷举所有有冲突的可能性,经过旋转完成修正。所有的冲突有6种,但是由于对称性,实际上就简化为3种。插入结点修正操作时,主要的考察对象是z.p的兄弟,即z的叔叔(uncle)结点,下面只介绍z.p是z.p.p的左子结点的情况,z.p是z.p.p的右子结点的情况对称。注意:sentinel结点是黑色的。令y=z.p.p.right:

       <a>z的叔叔结点是红色的:


图8 y的颜色是红色时的修正操作

       这时将z.p的颜色染成黑色,性质4被修复,但是性质5被破坏,所以将y也染成黑色,再将z.p.p的颜色染成红色,性质5也被修复(这里没有理解,C结点不染成红色也没问题啊,仅猜测这么做是为了将颜色冲突向根结点移动),并以它为新的z,设为z',进入下一次修复操作,当z'是根结点时,只要把它染成黑色即可,如果不是根结点,那么操作跟上述类似。

       <b>z的叔叔结点是黑色的,且z是z.p的右子结点

       <c>z的叔叔结点是黑色的,且z是z.p的左子结点

图9 y的颜色是黑色时的修正操作

       当z的叔叔结点是黑色的时候,两种情况以z是z.p的左子结点或者右子结点来区分。当z==z.p.right时,以z.p为支点左旋,成为另一种情况,这时没有解决颜色冲突,那么将z.p的颜色染成黑色,性质4被修复,但是性质5被破坏,所以将z.p.p染成红色,并且以它为支点右旋,性质5被修复。现在,z.p(即B结点)的父结点的染色不管是红色还是黑色都满足所有性质了,所以操作结束。

       修正操作的代码:

/*
 * Note: root may be modified.
 * Why perform inserting red node rather than black node?
 * 'cause inserting black node will always violate the 5th
 * property, while inserting red node will probably violate
 * the 2nd and the 4th properties. However, fixing up the
 * 2nd property is so easy and the 4th properties may be
 * not always violeated.
 */
static void rbt_insert_fixup(rbt_node **root, rbt_node *new)
{
	rbt_node *y = &sentinel;
	while(new->parent->color == RED) {
		if (new->parent == new->parent->parent->left) {
			y = new->parent->parent->right;
			if (y->color == RED) {
				new->parent->color = BLACK;
				y->color = BLACK;
				new->parent->parent->color = RED;
				new = new->parent->parent;
			} else {
				if (new == new->parent->right) {
					new = new->parent;
					rbt_left_rotate(root, new);
				}

				new->parent->color = BLACK;
				new->parent->parent->color = RED;
				rbt_right_rotate(root, new->parent->parent);
			}
		} else { /* symmetric to left */
			y = new->parent->parent->left;
			if (y->color == RED) {
				new->parent->color = BLACK;
				y->color = BLACK;
				new->parent->parent->color = RED;
				new = new->parent->parent;
			} else {
				if (new == new->parent->left) {
					new = new->parent;
					rbt_right_rotate(root, new);
				}

				new->parent->color = BLACK;
				new->parent->parent->color = RED;
				rbt_left_rotate(root, new->parent->parent);
			}
		}
	}

	(*root)->color = BLACK;
}
       因为修正操作不涉及被外部接口调用,所以定义为static的。

2.3 删除操作

       RBT的删除操作跟它的插入操作一样分为两部分,第一部分是删除结点操作,跟BST的结点删除操作类似,但是由于删除结点后可能会破坏某些性质,所以也要进行修正操作。

       由于删除操作事先是不知道要删除的结点的颜色的,如果要删除的结点是红色的,那么RBT的性质不会被破坏,但是如果要删除的结点是黑色的,那么RBT的性质2,性质4或者性质5有可能被破坏。

       删除结点操作的代码如下:

/*
 * Deleting red code will not violate properties.
 */
void rbt_delete(rbt_node **root, rbt_node *del)
{
	/*
	 * x keeps track of the node moves
	 * into y's original position
	 */
	rbt_node *y = del, *x = NULL, *r = NULL;
	rbt_color y_original_color = y->color;

	if (del->left == &sentinel) {
		x = del->right;
		r = rbt_transplant(root, del, del->right);
		rbt_node_free(&r);
	} else if (del->right == &sentinel) {
		x = del->left;
		r = rbt_transplant(root, del, del->left);
		rbt_node_free(&r);
	} else {
		// search for mini-key node in the right subtree
		y = rbt_minimum(del->right);
		y_original_color = y->color;
		x = y->right;

		// mini-key node is child of the to-be-deleted node
		if (y->parent == del)
			x->parent = y;
		else {
			/*
			 * do not need to free y 'cause del will be replaced with it
			 */
			rbt_transplant(root, y, y->right);
			y->right = del->right; // move y to the to-be-deleted node
			y->right->parent = y; // the original right child reparented to y
		}

		r = rbt_transplant(root, del, y);
		y->left = del->left; // y's left child now is the original left child
		y->left->parent = y; // the original left child reparented to y
		y->color = del->color;
		rbt_node_free(&r);
	}

	if (y_original_color == BLACK)
		rbt_delete_fixup(root, x);
}

       RBT删除操作的第一部分跟BST的删除操作类似,y_original_color保存了要删除的结点的颜色,如果它是红色的,不执行修正操作,因为没有性质被破坏;如果它是黑色的,那么就要执行修正操作了。因为RBT有sentinel结点,所以它的移植操作跟BST的稍微有点差异:

rbt_node *rbt_transplant(rbt_node **root, rbt_node *u, rbt_node *v)
{
	rbt_node *d = NULL;
	// child
	if (u->parent == &sentinel) {
		//u is root itself
		d = *root;
		*root = v;
	} else {
		d = u;

		if (u == u->parent->left)
			u->parent->left = v;
		else
			u->parent->right = v;
	}

	// parent
	v->parent = u->parent;

	return d;
}
       具体是NULL被sentinel取代,且v->parent = u->parent;前面不需要判断,因为sentinel结点肯定不为空。

       为了在修正过程中能保持性质5,可以假设取代要删除的结点的结点颜色有另外一层黑色(可以理解为它从被删除结点那儿继承来的),那么性质5没有被破坏,当取代的结点的颜色为红色时,那么取代的结点的颜色为“一红一黑”,为了不破坏性质1,它表示红色;如果取代的结点的颜色为黑色时,那么取代的结点的颜色为“双黑”,为了不破坏性质1,它表示黑色。前后有点矛盾,只用理解为外加的一层黑色是想象的,只是为了维持性质5,而非真的是两重色,在操作取代结点时,仍然以它本身的颜色为准。

       修正操作的参考结点是取代结点的兄弟结点和侄子结点。修正操作有8种情况,但是由于对称性,实际上只有4种情况,这里只以取代结点(x)是其父结点的左子结点为例:

       

图10 删除结点后的修正操作情况

       <a>取代结点的兄弟结点是红色的。

       <b>取代结点的兄弟结点是黑色的,且其两个侄子结点都是黑色的。

       <c>取代结点的兄弟结点是黑色的,且其左侄子结点颜色是红色的,其右侄子结点颜色是黑色的。

       <d>取代结点的兄弟结点是黑色的,且其右侄子结点颜色是红色的(左子结点颜色任意)。

       注意:图中黑色表示“Black”,灰色表示“Red”,白色表示“Red or Black”。由于取代结点和其兄弟结点的颜色都是黑色的时候,无法判断它们的父结点是什么颜色,因为在删除结点之前,它们的父结点的颜色不管是红色还是黑色,都满足RBT的性质。

       对于<a>情况,将取代结点(x,下同)的兄弟结点染成黑色,将其父结点染成红色,再以x的父结点为支点左旋。最后将新的参考结点指向x的兄弟结点,即x的兄弟结点成为黑色结点了,可以进入情况<b>,<c>和<d>。

       对于<b>情况,将x的兄弟结点染成红色,并将参考结点指向其父结点,将颜色冲突向根部移动。

       对于<c>情况,将x的左侄子结点染成黑色,将x的兄弟结点染成红色,以x的兄弟结点为支点右旋,并将参考结点指向x的兄弟结点。

       对于<d>情况,将x的右侄子结点染成黑色,将x的兄弟结点染成其父结点的颜色,再将其父结点染成黑色,以其父结点为支点左旋,然后将新的x指向T.root。重点讲下这个:因为x这条路径删除了个黑色结点,那么这条路径上就少了个黑色结点,破坏性质5,将x的兄弟结点染成其父结点的颜色,且把其父结点和右侄子结点染成黑色,再进行左旋,那么x原来的兄弟结点的路径上黑色结点没有改变(x的右侄子结点染成黑色),x所在的路径多了一个黑色的结点(x的父结点染成黑色,并左旋到x的路径上),所以两条路径上的黑色结点数相同了。

       与插入操作的修正操作一样,不涉及被外部调用,所以定义为static,下面是修正操作的代码:

/*
 * Note: when root is to be deleted, its pointer will be modified.
 * Deleting red node will not violate properties.
 */
static void rbt_delete_fixup(rbt_node **root, rbt_node *node)
{
	rbt_node *w = &sentinel;

	while (node != *root && node->color == BLACK) {
		if (node == node->parent->left) {
			w = node->parent->right;
			// case 1
			if (w->color == RED) {
				w->color = BLACK;
				node->parent->color = RED;
				rbt_left_rotate(root, node->parent);
				w = node->parent->right;
			}

			// case 2
			if (w->left->color == BLACK && w->right->color == BLACK) {
				/* case 2 --> case 1
				 * now w's color is not cleared
				 */
				w->color = RED;
				node = node->parent;
			} else {
				// case 3
				if (w->right->color == BLACK) {
					w->left->color = BLACK;
					w->color = RED;
					rbt_right_rotate(root, w);
					w = node->parent->right;
				}

				// case 4
				w->color = node->parent->color;
				w->right->color = BLACK;
				node->parent->color = BLACK;
				rbt_left_rotate(root, node->parent);
				node = *root; // what the fuck?
			}
		} else {
			w = node->parent->left;
			// case 1
			if (w->color == RED) {
				w->color = BLACK;
				node->parent->color = RED;
				rbt_right_rotate(root, node->parent);
				w = node->parent->left;
			}

			// case 2
			if (w->left->color == BLACK && w->right->color == BLACK) {
				/* case 2 --> case 1
				 * now w's color is not cleared
				 */
				w->color = RED;
				node = node->parent;
			} else {
				// case 3
				if (w->left->color == BLACK) {
					w->right->color = BLACK;
					w->color = RED;
					rbt_left_rotate(root, w);
					w = node->parent->left;
				}

				// case 4
				w->color = node->parent->color;
				w->left->color = BLACK;
				node->parent->color = BLACK;
				rbt_right_rotate(root, node->parent);
				node = *root;
			}
		}
	}

	node->color = BLACK;
}
       到此为止,红黑树的各种操作的C实现就基本结束了,其中序遍历代码如下:
void rbt_inorder_walk(rbt_node *root)
{
	rbt_node *y = root;

	if (y != &sentinel) {
		rbt_inorder_walk(y->left);
		if (y->color == RED)
			printf("<RED ");
		else
			printf("<BLACK ");
		printf(" key: %d> ", y->key);
		rbt_inorder_walk(y->right);
	}
}
       验证时用到了下列网址的示意图,在此表示感谢:

       http://saturnman.blog.163.com/blog/static/557611201097221570/

       根据示意图,结合插入和删除的遍历打印,已经验证代码可以正确执行,但未考虑执行效率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值