红黑树 red-black tree:
红黑树介绍:
红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由鲁道夫·贝尔发明的,他称之为"对称二叉B树",它现代的名字是在 Leo J. Guibas 和 Robert Sedgewick 于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n是树中元素的数目。
红黑是一棵二叉搜索树,它在每个节点上增加了一个存储位来表示节点颜色,可以使RED或BLACK。通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出2倍,所以它是近似平衡的。
红黑树是一种好的搜索树:
引理:一棵有n个内部节点的红黑树的高度至多为2lg(n+1)。
红黑树节点包含5个属性:color、key、left、right和parent。如果一个节点没有子节点或父节点,则该节点相应指针值为NIL。我们可以NIL看做叶节点,把带关键字的节点视为内部节点。
红黑树定义:
typedef int color_t; /* red black tree color's type */
typedef int key_t; /* red black tree key's type */
typedef struct red_black_node RBTnode_t; /* red black tree node type */
struct red_black_node
{
color_t color; /* node's color */
key_t key; /* node's key */
RBTnode_t *left; /* node's left child */
RBTnode_t *right; /* node's right child */
RBTnode_t *p; /* node's parent */
};
typedef struct RedBlackTree RBTree; /* red black tree type */
struct RedBlackTree
{
RBTnode_t *root; /* root node */
RBTnode_t *nil; /* guard of red black tree(leaf node) */
};
红黑树的性质:
1.每个节点或是红色,或是黑色。
2.根节点是黑色。
3.每个叶节点(NIL节点)是黑色。
4.如果一个节点是红色,则它的两个子节点都是黑色。
5.对于每个节点,从该节点到它所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
图a是一棵红黑树例子:
图a
图b是对图a红黑树的一种简单改变:
图b
图b中为了方便处理红黑树的边界条件,图b中用一个哨兵节点T.nil节点来代替图a中所有的NIL节点,并将树的根节点的父节点指向T.nil节点。哨兵节点的颜色总是黑色。
红黑树黑高定义black-height:
从某个节点x处发(不包括该节点)到达一个叶节点的任意一条简单路径上的黑色节点个数称该节点黑高,记为bh(x)。
红黑树的插入:
左旋与右旋:
红黑树的插入与删除操作可能会违反红黑树的性质。所有需要对插入与删除操作进行一些额外的维护来保证其性质成立。在维护过程中,我们需要两个操作:左旋、右旋。
图c是左旋与右旋的示意图:
在内部节点x上左旋时假定其右节点不是叶子节点。同样,在内部节点y上右旋时假定其左孩子不是叶子节点。
图c
下面是左旋与右旋的代码:
void left_rorate(RBTree *T, RBTnode_t *node) /* 在node节点上左旋 */
{
RBTnode_t *tmp;
tmp = node->right;
node->right = tmp->left;
if(tmp->left != T->nil)
tmp->left->p = node;
tmp->p = node->p;
if(node->p == T->nil)
T->root = tmp;
else if(node->p->left == node)
node->p->left = tmp;
else
node->p->right = tmp;
node->p = tmp;
tmp->left = node;
}
void right_rotate(RBTree *T, RBTnode_t *node) /* 以node为根子树右旋 */
{
RBTnode_t *tmp;
tmp = node->left;
node->left = tmp->right;
if(tmp->right != T->nil)
tmp->right->p = node;
tmp->p = node->p;
if(node->p == T->nil)
T->root = tmp;
else if(node->p->left == node)
node->p->left = tmp;
else
node->p->right = tmp;
node->p = tmp;
tmp->right = node;
}
插入:
红黑树的新节点插入可以在O(lg n)时间内完成。
对于插入操作来说,将新节点颜色设置为红,这样可能会破坏红黑树的两个性质:
1.每个节点或是红色,或是黑色。
4.如果一个节点是红色,则它的两个子节点都是黑色。
对于选择将新节点颜色设置为红色,是因为如果设为黑色,就会导致根到叶子的路径上有一条路上,多一个额外的黑节点,破坏性质5,这个是很难调整的。但是设为红色节点后,可能会导致破坏性质1,或破坏性质4,而这两种破坏比较容易调整。
其实两种破坏情况是互斥的。因为如果破坏性质1,那么插入节点的必定一个根节点,根节点的父节点设置为哨兵节点(哨兵节点颜色总是黑),所以不会破坏性质4;如果破坏性质4,那么插入的不会是一个根节点。
对于破坏性质1,那么只需要把根节点颜色改为黑色就行了。
对于破坏性质4,则要麻烦一点,这儿用insert_fixup来对插入后的红黑树进行修复:
可以分为两种情况:
1.节点的叔节点是红色2.节点的叔节点是黑色2.1节点的父节点是祖父节点右子树(该情况可以转换为情况2.2)2.2节点的父节点是祖父节点左子树
下面的是3情况的示意图,图中都忽略了哨兵节点T.nil
case 1:
插入新节点z:
图1
修复方法:
将新节点z的父节点涂黑,祖父节点涂红,这样对于子树7来说,其黑高不变,而且它符合性质4。但是这样可能会在z节点处产生破坏性质4或者1的情况。所以我们将z指向它的祖父节点,判断这时z子树是否破坏红黑树性质。如果z节点父节点为黑色,则调整完成;否则z父节点为红色,再次调用算法来调整。
下面是图1对情况1的第一次调整后的图,可以看见现在的z节点与父节点仍然不符合性质4要求。
case 2.1
图2
这时图2中刚好符合破坏情况2.1。对于这种情况(z子树是一棵右子树),将z指向其父节点,然后对z节点左旋转,将情况2.1换为情况2.2。如图3:
case 2.2
图3
可以看到,在图3中,z现在指向了图2中7号节点的父节点2,然后左旋后得到的图。
可以看得出来,这样旋转后情况符合2.2,而且没有破坏其它性质。
调整情况3:
将z的父节点设置为黑色,祖父节点设置为红色。然后将z指向祖父节点,对z为根的子树进行右旋。操作后得到图5的状态:
我们先分析一下情况3的调整操作:
这是图3直接旋转后得到的状态图:
图4
对于子树z、b、y来说,旋转操作不改变它们的黑高,而图中整个子树来说,从左子树到叶节点,黑高减少了1;而从右子树到叶节点,黑高增加了1。可以明显看出,只要将z节点改为黑色,x节点改为红色,这样z子树的左右两边的黑高就平衡了。而且由于叔节点y是黑色,b节点也是黑色(因为他是a的子节点点,而a节点是红色,性质4)。所以改变x节点颜色不会破坏红黑树性质。
图5
这时可以看到,整棵子树已经已经符合了红黑树的性质,而且在修复操作后,整棵子树的黑高保持不变,所有调整已经完成。
对红黑树的删除与插入操作复杂度分析:
因为每一个红黑树也是一个特化的二叉查找树,因此红黑树上的只读操作与普通二叉查找树上的只读操作相同。然而,在红黑树上进行插入操作和删除操作会导致不再符合红黑树的性质。恢复红黑树的属性需要少量(O(log n))的颜色变更(实际是非常快速的)和不超过三次树旋转(对于插入操作是两次)。虽然插入和删除很复杂,但操作时间仍可以保持为 O(log n) 次。
下面是插入操作的源代码:
/*
* RBT_search: 在树中查找一个值为指定参数的位置
* key : 查找值
* pos : 如果key存在树中,则pos指向key所做节点位置,否则指向key应插入位置的父节点
* 返回值:key存在返回TRUE, 否则返回FALSE
*/
int RBT_search(RBTree *T, key_t *key, RBTnode_t **pos)
{
assert(T);
RBTnode_t *pp = T->nil;
RBTnode_t *p = T->root;
while(p != T->nil)
{
pp = p;
if(p->key > *key)
p = p->left;
else if(p->key < *key)
p = p->right;
else
{
*pos = p;
return TRUE; /* 找到key值节点 */
}
}
*pos = pp; /* 未找到key值节点 */
return FALSE;
}
/*
* RBT_insert: 在树中插入指定节点
* T : 节点插入的树
* node : 插入的节点
* 返回值:成功插入返回TRUE,否则返回FALSE
*/
int RBT_insert(RBTree *T, RBTnode_t *node) /* 插入node节点 */
{
RBTnode_t *pos;
if(RBT_search(T, &node->key, &pos) == TRUE) /* pos为新节点的父节点 */
{
printf("Insert node has already.\n");
return FALSE;
}
node->p = pos;
if(pos == T->nil)
T->root = node;
else if(node->key < pos->key)
pos->left = node;
else
pos->right = node;
node->color = RED;
node->left = node->right = T->nil;
insert_fixup(T, node); /* 调整 */
return TRUE;
}
/*
* insert_fixup: 对RBT_insert操作后,调整树,使其符合红黑树性质
* T : 指定操作树
* node : 插入的节点
*/
void insert_fixup(RBTree *T, RBTnode_t *node) /* 辅助插入操作 */
{
RBTnode_t *uncle;
while(node->p->color == RED) /* 规则4被破坏 */
{
/*
*对于node节点的父节点是一个左孩子还是右孩子需分别调整
*/
if(node->p == node->p->p->left) /* 父节点为祖父节点的左孩子 */
{
uncle = node->p->p->right;
if(uncle->color == RED)//case 1
{
node->p->color == BLACK;
uncle->color = BLACK;
node->p->p->color = RED;
node = node->p->p;
}
else
{
if(node == node->p->right) //case 2.1
{
node = node->p;
left_rorate(T, node);
}
node->p->color = BLACK; //case 2.2
node->p->p->color = RED;
right_rotate(T, node->p->p);
}
}
else /* 父节点为祖父节点的左孩子 */
{
uncle = node->p->p->left;
if(uncle->color == RED) //case 1
{
node->p->color = BLACK;
uncle->color = BLACK;
node->p->p->color = RED;
node = node->p->p;
}
else
{
if(node == node->p->left) //case 2.1
{
node = node->p;
right_rotate(T, node);
}
node->p->color = BLACK; //case 2.2
node->p->p->color = RED;
left_rorate(T, node->p->p);
}
}
}
T->root->color = BLACK; /* 保证性质1不被破坏 */
}
在insert_fixup操作中,我们使用node->p->color == RED来控制循环,这样对操作根节点时,由于根节点的父节点指向的是T.nil节点,而T.nil节点颜色总是黑色,所以可以正常结束循环,并且在循环后放置了一条语句:T->root->color = BLACK,使红黑树的性质1不会被破坏掉。