数据结构:红黑树原理分析和C语言实现分步详解

红黑树原理

什么是红黑树

红黑树是一种特殊的二叉查找树,用来实现对大批量有序数据的管理操作。
二叉查找树,即是特殊的二叉树,树中每个节点的左子树的所有元素都不大于该节点的元素,右子树的所有元素都不小于该节点。
相比普通的二叉查找树,红黑树中的每个节点增加了色域(color field)这一数据项,色域取值可以是红色或者黑色。通过巧妙的设计,红黑树可以保证在任何情况下,对数据的有序插入,查找和删除操作,都能在O(logn)的时间复杂度内完成。

为什么要用红黑树

红黑树是一种性能优异的数据结构,经常用作其他数据结构如map等的底层实现。二叉查找树,包括随机二叉查找树,依然会在某些极端情况下,表现出O(n)的时间复杂度,并不能保证总是高效的处理数据。而红黑树,作为一种平衡查找树,可以保证整棵树的高度不超过2log(n+1),从而保证了时间复杂度限制在O(logn),保证了算法在最坏情况下表现依然能满足我们所期望的效率要求。

红黑树的性质(Red-Black properties)

红黑树的优秀性能,依靠如下性质来保证,满足如下性质的二叉查找树,即是红黑树。

  1. 每个节点都是红色或者黑色。
  2. 根节点和叶子节点都是黑色。
  3. 红色节点的子节点或者父节点都必须是黑色。
  4. 每个节点到其任一后代叶子节点的所有简单路径上的黑色节点数目相同。

对上述性质进行如下说明:

  1. 红黑树在二叉树的基础上,将每个叶子节点的空指针替换为nil节点作为新的叶子节点,这些nil节点称为外部节点(我们只关心外部节点的颜色,对他们的其他性质没有要求,因此可以用一个哨兵节点统一代替所有的外部节点)而原有节点称为内部节点。
  2. 性质3要求红色节点的父节点或者子节点为黑色,即任何一条从根到叶子的简单路径上不能出现两个连续的节点是红色。
  3. 根节点和叶子节点要求为黑色,而不能为红色,是为了避免给内部节点添加不必要的限制。
  4. 任一节点到其后代叶子节点的简单路径上的黑色节点数称为黑高(black height),叶子节点的黑高为0,黑高不计入该节点本身的颜色。

通过上述的性质规约,可以证明红黑树的操作时间复杂度为O(logn),n为内部节点的数量(数据的总量)。

首先通过归纳法证明,对任意一节点x,以其为根的子树中至少含有2bh(x)-1个内部节点。bh(x)为黑高。

  1. 对于叶子节点,bh(x) = 0,2bh(x)-1=0, 条件成立。
  2. 对任意内部节点x,其子节点的黑高为bh(x)(当子节点是红色节点)或者bh(x)-1(当子节点是黑色节点),则x节点的子节点至少含有2bh(x)-1-1个内部节点,故x为根的子树含有1+2bh(x)-1-1+2bh(x)-1-1=2bh(x)-1个内部节点。
  3. 设h为树的高度,根据红黑树的性质2,任何一条根到叶子的简单路径上的黑色节点至少有一半,即根的黑高至少为h/2,故有n>=2h/2-1,即h<=2log(n+1)。

而二叉查找树的增删查改时间复杂度均为O(h),从而我们可以保证红黑树在任意情况下都有O(logn)的时间复杂度。

C语言实现

红黑树优良的效率性能。是牺牲了算法上的复杂度。在进行增删操作时,需要对红黑树加以修正来满足红黑树的四条性质。主要通过二叉树的旋转和节点的变色来实现。

同时要注意,在插入新节点的时候,我们默认新节点的颜色为红色,这是因为,插入黑色节点,必然会改变某些简单路径上的黑色节点数目,很有可能使得所有的简单路径黑色节点数目不等,带来额外的操作调整。

在本实现中,采用了哨兵节点来取代所有的NULL指针,简化边界条件。

数据结构定义部分

//头文件部分
#define RED        0    // 红色节点
#define BLACK    1    // 黑色节点

typedef int Type;	//支持后续数据类型扩展

// 红黑树的节点
typedef struct RBTreeNode{
    unsigned int color;        // 颜色(RED 或 BLACK)
    Type   key;                    // 关键字(键值)
    struct RBTreeNode *left;    // 左孩子
    struct RBTreeNode *right;    // 右孩子
    struct RBTreeNode *parent;    // 父结点
}Node;
typedef struct {
	Node *root;	//根节点
	Node nil;	//哨兵元素
}RBTree;

创建红黑树

RBTree * init_tree()
{
	RBTree *tree = (RBTree*)malloc(sizeof(RBTree));
	tree->nil.color = BLACK;
	tree->root = &tree->nil;
	tree->root->parent = &(tree->nil);
	return tree;
}
初始化一个空节点
Node*  init_node(Type key)
{
    Node *bt =(Node*)calloc(1,sizeof(Node));
    bt->color = RED;
    bt->right = NULL;
    bt->left = NULL;
    bt->key = key;
    bt->parent = NULL;
    return bt;
}

节点插入

  • 插入过程
  • 节点插从树根开始查找,遇到叶子节点nil后插入‘
  • 如果该叶子节点的父节点为nil,说明树为空,将插入的节点设为根节点;
  • 通过变色和旋转操作修正红黑树使其满足红黑性质。
插入函数
void insert_node(RBTree *tree, Node *node)
{
	Node *p,*n;
	p = tree->root->parent;	//指示父节点
	n = tree->root;	//寻找插入位置
	while(*n != tree->nil)
	{
		p = n;
		if(node->key < n->key)
		{
			n = n->left;
		}
		else
		{
			n = n->right;
		}
	}
	//循环终止时,n为nil节点,p为n的父节点,用node取代n的位置
	node->parent = p;
	if(&p == tree->nil)
	{
		tree->root = node;
	}
	else
	{
		if(node->key < p->key)
		{
			p->left = node;
		}
		else
		{
			p->right = node;
		}
	}
	node->color = RED;
	node->left = &tree->nil;
	node->right = &tree->nil;
	rb_fix(tree, node)	
}

红黑修正函数

增加新的红色节点时,可能导致如下情况破坏红黑性质:

  1. 根节点变为红色
  2. 插入节点的父节点也是红色

情况1发送在红黑树原本为空的情况下,只需要对根节点进行变色处理即可。
情况2需要考虑到新增节点的叔父节点的情况,因此大体分为三种情形:

  1. 叔父节点和父节点均为红色,则将叔父节点和父节点均变为黑色,同时为了保证性质三,需要调整祖父节点为红色。然后将祖父节点作为新增节点进行迭代;
  2. 叔父节点是黑色,新增节点,父节点,祖父节点形成"<“或”>“形状,即新增节点是左孩子,父节点是右孩子,或者新增节点是右孩子,父节点是左孩子,则对父节点进行左旋或者右旋,变为”/“或”\",即变成第三种情况,在这种情况下,父子节点角色互换。
  3. 叔父节点是黑色,新增节点,父节点,祖父节点为"/“或”\"构型,只需要对祖父节点进行右旋或左旋操作,将父节点作为祖父节点涂黑,孩子节点和祖父节点作为两个兄弟节点涂红,从而保证了性质4不被破坏

左旋和右旋操作,是二叉搜索树的基本操作之一

/*
 左旋示意图(对节点x进行左旋):
      px                              px
     /                               /
    x                               y
   /  \      --(左旋)-->           / \                #
  lx   y                          x  ry
     /   \                       /  \
    ly   ry                     lx  ly
*/
void rotate_left(Node * x, RBTree *tree)
{
	Node *y = x->right, *lx = x->left, *ly = y->left, *ry = y->right;
	Node t;
	//将x,lx节点信息暂存
	t.color = x->color;
	t.key = x->key;

	//用y节点信息取代x
	x->color = y->color;
	x->key = y->key;
	x->right = y->right;
	if ( &tree->nil!= y->right)
	{
		y->right->parent = x;
	}
	x->left = y;

	//x节点信息转移到y节点
	y->color = t.color;
	y->key = t.key;
	y->left = lx;
	if (&tree->nil != lx)
	{
		lx->parent = y;
	}
	y->right = ly;
}
/*
 右旋示意图(对节点y进行左旋):
            py                               py
           /                                /
          y                                x
         /  \      --(右旋)-->            /  \ 
        x   ry                           lx   y
       / \                                   / \   
      lx  rx                                rx  ry
*/

void rotate_right(Node *y, RBTree *tree)
 {
	Node *x = y->left, *lx = x->left, *ry = y->right, *rx = x->right;
	Node t;
	//y存于t
	t.color = y->color;
	t.key = y->key;
	//x取代y
	y->color = x->color;
	y->key = x->key;
	y->left = lx;
	if (&tree->nil != lx)
	{
		lx->parent = y;
	}
	y->right = x;

	//y取代x
	x->color = t.color;
	x->key = t.key;
	x->left = rx;
	x->right = ry;
	if (&tree->nil != ry)
	{
		ry->parent = x;
	}
}
//返回祖父节点,采用宏定义的形式更高效
Node * grand(Node *n)
{
	return n->parent->parent;
}
//返回叔父节点
Node * uncle(Node *n)
{
	if (n->parent == grand(n)->left)
		return grand(n)->right;
	else
		return grand(n)->left;
}

void rb_fix(RBTree *tree, Node *node)
{
	//根节点为红,变色
	if(tree->root->color == RED)
	{
		tree->root->color = BLACK;
		return;
	}
	//父节点为黑色。无需修正
	if(node->parent->color == BLACK)
	{
		return;
	}
	//情形1
	if(uncle(node)->color == RED)
	{
		uncle(node)->color = BLACK;
		node->parent->color = BLACK;
		grand(node)->color = RED;
		rb_fix(tree,grand(node));
		return;
	}
	//情形2.父子节点互换,node为父节点
	if(node==node->parent->left&&uncle(node)==grand(node)->left ||
	node==node->parent->right&&uncle(node)==grand(node)->right)
	{
		if(node == node->parent->left)
		{
			rotate_right(node->parent, tree);
			rb_fix(node->right);	//	原父节点现在是右孩子
		}
		else
		{
			rotate_left(node->parent, tree);
			rb_fix(node->left);
		}
		return;
	}
	//情形3
	if(node==node->parent->left&&node->parent==grand(node)->left ||
	node==node->parent->right&&node->parent==grand(node)->right)
	{
		node->parent->color = BLACK;
		grand(n)->color = RED;

		if(node == node->parent->left)
		{
			rotate_right(node->parent->parent, tree);
		}
		else
		{
			rotate_left(node->parent->parent, tree);
		}
		return;
	}
}

节点的删除

对于红黑树删除节点,删除过程和二叉查找树删除过程大致相同,最大的区别就在于删除后同样需要对红黑树进行修正来满足红黑性质。

  1. 首先,如果待删除节点有两个非nil的子节点,那么需要转化成至多只有一个非nil子节点的删除问题。方法就是找到该节点的前驱或者后继节点,删除它的前驱或者后继节点,并把其值放入该节点中。
  2. 对于至多只有一个非nil子节点的待删除节点,如果该节点是红色,只需要用它的一个黑色儿子进行替补,无需修正。
  3. 如果是黑色的待删除节点,则在用其子节点替补后,需要进行红黑修正
寻找后继节点

后继节点即右子树的最小元素

Node *find_next(Node *node, RBTree *tree)
{
	Node *n = node->right;
	while(n->left != &tree->nil)
	{
		n  = n->left;	
	}
	return n;
}
节点取代函数
//d为被取代的节点,n为取代节点
void transplant(Node *d, Node *n, RBTree *tree)
{
//被取代节点是根节点
	if(d->parent == &tree->nil)
	{
		tree->root = n;
		return;
	}
	if(d->parent->left == d)
	{
		d->parent->left = n;
	}
	else
	{
		d->parent->right = n;
	}
}
删除节点函数
void delete_node(RBTree *tree, Node *n)
{
	//保存被删除节点的颜色,用来作为是否需要修正的条件
	unsigned int flag = n->color;
	//保存取代节点
	Node *r;
	//两个子节点都是内部节点,用后继节点代替该节点,删除后继节点
	if(n->left != &tree->nil && n->right != &tree->nil)
	{
		n->key = find_next(n,tree)->key;
		delete_node(tree, find_next(n,tree));
		return;
	}
	//左孩子是nil
	if(n->left == tree->nil)
	{
		 r = n->right;
		 transplant(n,r,tree);
	}
	//右孩子是nil
	else
	{
		r = n->left;
		 transplant(n,r,tree);
	}
	if(flag == BLACK)
	{
		rb_delete_fix(tree, r);
	}
}
红黑树修正函数

红黑树修正函数接受新的替代节点作为参数,而上述分析可知,该节点是被删除节点的子节点。仅当被删除节点是黑色节点时才会调用修正函数,故分以下情况进行讨论。
需要修正的原因是经过新节点的路径比其他路径少了一个黑色节点

  1. 修正节点是红色节点,涂黑即可替代原来的黑色节点保持红黑性质;
  2. 修正节点是黑色节点,且是根节点,则无需修正;
  3. 修正节点是黑色节点,兄弟节点、兄弟节点的子节点都是黑色,父节点也是黑色,则重绘兄弟节点为红色,此时相当于经过父节点的路径都少了一个黑色节点,把父节点作为修正节点进行迭代;
  4. 修正节点是黑色节点,兄弟节点、兄弟节点的子节点都是黑色,父节点是红色,则调换兄弟节点和父节点的颜色,相当于通过修正节点路径的黑色节点数目增加1,从而完成修正
  5. 修正节点是黑色节点,且是左(右)节点,兄弟节点是黑色,兄弟的右(左)孩子是红色,左(右)孩子无要求:对父节点左(右)旋,使得兄弟节点成为新的父节点,原来的父节点取代修正节点N,红色的孩子节点取代兄弟节点的位置。交换父节点和兄弟节点的颜色,并重绘红色孩子为黑色节点。则相当于在原来修正节点的路径上增加了父节点作为黑色节点,原来的红色孩子节点变黑,填补了上位的兄弟节点留下的空位。
    在这里插入图片描述
  6. 修正节点是黑色节点,且是左(右)节点,兄弟节点是黑色,兄弟的左(右)孩子是红色,右(左)孩子是黑色,对兄弟节点进行一次右(左)旋,使得黑色子节点取代兄弟节点的位置,红色子节点取代原来黑色子节点的位置,交换原兄弟节点和红色子节点的颜色,构成情况5,迭代调用。
    在这里插入图片描述
  7. 修正节点是黑色节点,兄弟节点是红色节点,对父节点做旋转操作,将兄弟节点转到祖父节点位置,兄弟节点的一个儿子节点成为修正节点的新的兄弟节点,然后调换原来的兄弟节点和父节点的颜色,此时修正节点有了新的红色父节点和黑色兄弟节点,可用情况4,5,6迭代。
    在这里插入图片描述
    代码如下:
//求兄弟节点
Node * sib(Node *n)
{
	if(n == n->parent->left)
	{
		return n->parent->right;
	}
	else
	{
		return n->parent->left;
	}
}
//修正函数
void rb_delete_fix(RBTree *tree, Node *r)
{
	//情况1,修正节点是红色
	if(r->color == RED)
	{
		r->color == BLACK;
		return;
	}
	//情况2
	if(r == tree->root)
	{
		return;
	}
	//情况7,兄弟节点为红色
	if(sib(r)->color == RED)
	{
		sib(r)->color = BLACK;
		r->parent->color = RED;
		if(r == r->parent->left)
		{
			rotate_left(r->parent, tree);
		}
		else
		{
			rotate_right(r->parent, tree);
		}
		rb_delete_fix(tree, r);
		return;
	]
	//情况3,兄弟节点的子节点都是黑色,父节点也是黑色
	if(sib(r)->left->color == BLACK && sib(r)->right->color == BLACK && r->parent->color == BLACK)
	{
		sib(r)->color = RED;
		rb_delete_fix(tree, r->parent);
		return;
	}
	//情况4,兄弟节点的子节点都是黑色,父节点是红色
	if(sib(r)->left->color == BLACK && sib(r)->right->color == BLACK && r->parent->color == RED)
	{
		sib(r)->color = RED;
		r->parent->color = BLACK;
		return;
	}
	//情况5,兄弟有一个节点是红色,且红色节点的左右位置和修正节点的左右位置相对
	if(r->parent->left == r && sib(r)->right->color == RED || r->parent->right == r && sib(r)->left->color == RED)
	{
		sib(r)->color = r->parent->color;
		r->parent->color = BLACK;
		if(r==r->parent->left)
		{
			sib(r)->right->color = BLACK;
			rotate_left(r->parent,tree);
		}
		else
		{
			sib(r)->left->color = BLACK;
			rotate_right(r->parent,tree);
		}
		return;
	}
	//情况6,可以不写判断条件,因为其上的分支已经把其他所有情况进行了处理
	if(sib(r)->left->color == RED)
	{
		sib(r)->color = RED;
		sib(r)->left->color = BLACK;
		rotate_right(sib(r), tree);
	}
	else
	{
		sib(r)->color = RED;
		sib(r)->right->color = BLACK;
		rotate_left(sib(r), tree);
	}
	rb_delete_fix(r,tree);
}

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章