一、前言
最近在看算法导论,关于红黑树看了许久,才有个大概的影子,关键的部分还是很多未能完全理解,这篇博客相当于学习笔记吧,后续有更好的理解或者感悟在补上,由此不得不佩服红黑树的发明者。
二、红黑树性质
在这里我们会想到既然是平衡树,我们在数据结构当中学过平衡二叉树(AVL),为什么还要引入红黑树呢?由于笔者功力尚浅,两者的区别就不做很细致的比较,下面给出一个比较直观大体的理解;
(1)在插入一个节点时,红黑树和AVL树所要进行的旋转次数最多只要2次旋转,也就是O(1)的时间复杂度,两者区别并不明显
(2)在删除节点时,AVL树在最坏情况下,要维护从被删节点到root节点这一条路径上的所有节点的平衡性,时间复杂度为O(logN)。然而红黑树最多执行3次旋转操作,也就是O(1)的时间复杂度
(3)AVL树是严格的平衡二叉树,所以存在大量次数的插入、删除操作,回引起AVL树的频繁调整,调整的频率远多于红黑树
所以,综合很多情况,采用红黑树来代替AVL树实现一些集合的动态操作是一种折中的考虑。
好了,回到我们的主题上,本文讨论的是红黑树,扯了会儿AVL树,此举也是先消除读者的疑虑,现在我们重点讨论红黑树,下面我们给出红黑树的基本性质:
1) 每个节点的颜色或是红色,亦或是黑色;
2) 根节点时黑色;
3) 每个指向NIL的叶节点是黑色的;
4) 如果一个节点时红色,则相应的子节点必须是黑色的;
5) 对于每个节点,从该节点到其所有该节点后代叶节点的简单路径上,包含相同数目的黑色节点;
红黑树示例:
证明:
令树的高度是h,从根节点到叶节点的任何简单路径上拥有至少一半的节点为黑色,因此,根的黑高至少为h/2;于是有N >= 2^(h/2) - 1, 于是可以得出h<=2log(N+1)
三、左右旋转
左旋右旋代码表示:
/*
* 对红黑树的节点(x)进行左旋转
*
* px px
* / /
* x y
* / \ --(左旋)--> / \
* lx y x ry
* / \ / \
* ly ry lx ly
*
*/
void rbtree_left_rotate(RBTRoot *root, Node * x){
Node *y = x->right;
x->right = y->left;
if(y->left != NULL){
y->left->parent = x;
}
y->parent = x->parent;
if(x->parent == NULL){
root->node = y;
} else if(x == x->parent->left){
x->parent->left = y;
}else{
x->parent->right = y;
}
y->left = x;
x->parent = y;
}
/*
* 对红黑树的节点(y)进行右旋转
*
* 右旋示意图(对节点y进行左旋):
* py py
* / /
* y x
* / \ --(右旋)--> / \
* x ry lx y
* / \ / \
* lx rx rx ry
*
*/
void rbtree_right_rotate(RBTRoot *root, Node *y){
Node *x = y->left;
y->left = x->right;
if(x->right != NULL){
x->right->parent = y;
}
x->parent = y->parent;
if(y->parent == NULL){
root->node = x;
}else if(y == y->parent->left){
y->parent->left = x;
}else{
y->parent->right = x;
}
x->right = y;
y->parent = x;
}
四、插入操作
将一个节点(z)插入到红黑树中,首先,将红黑树当作一颗二叉查找树,将节点插入相应的位置;然后,将节点着色为红色;最后,通过"旋转和重新着色"等一系列操作来修正该树,使之重新成为一颗红黑树,满足红黑树的性质。详细描述如下:
第一步: 将红黑树当作一颗二叉查找树,将节点插入。
红黑树本身就是一颗二叉查找树,将节点插入后,该树仍然是一颗二叉查找树。也就意味着,树的键值仍然是有序的。此外,无论是左旋还是右旋,若旋转之前这棵树是二叉查找树,旋转之后它一定还是二叉查找树。这也就意味着,任何旋转和重新着色操作,都不会改变它仍然是一颗二叉查找树的事实。
第二步:将插入的节点着色为"红色"。
为什么着色成红色,而不是黑色呢?为什么呢?在回答之前,我们需要重新温习一下红黑树的性质:
(1) 每个节点或者是黑色,或者是红色。
(2) 根节点是黑色。
(3) 每个指向NIL的叶节点是黑色的;
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5) 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
为什么要将插入节点着为红色?
因为插入之前所有根至叶子节点的路径上黑色节点数目都相同,所以如果插入的节点是黑色肯定错误(黑色节点数目不相同),而相对的插入红节点可能会也可能不会违反“没有连续两个节点是红色”这一条件,所以插入的节点为红色,如果违反条件再调整将插入的节点着色为红色,不会违背"性质(5)",少违背一条特性,就意味着我们需要处理的情况越少。
第三步:通过一系列的旋转或着色等操作,使之重新成为一颗红黑树。
第二步中,将插入节点着色为"红色"之后,不会违背"特性(5)"。那它到底会违背哪些特性呢?
对于"性质(1)",显然不会违背了。因为我们已经将它涂成红色了。
对于"性质(2)",显然也不会违背。在第一步中,我们是将红黑树当作二叉查找树,然后执行的插入操作。而根据二叉查找数的特点,插入操作不会改变根节点。所以,根节点仍然是黑色。如果违反了性质2,则插入的节点一定是新增的根节点,是树中唯一的内部节点,
对于"性质(3)",显然不会违背了。这里的叶子节点是指的空叶子节点,插入非空节点并不会对它们造成影响。
对于"性质(4)",是有可能违背的!
那接下来,想办法使之"满足性质(4)",就可以将树重新构造成红黑树了。
第四步:考虑多种情况将违反第四条原则的红色树调整为满足所有性质的合法红黑树
实际在调整的过程中需要考虑六种情况,这取决于插入节点z的父节点是其祖父节点的左子节点还是右子节点,对于这两种情况是对称的,所有接下来我们考虑插入节点的父节点是其祖父节点的左子节点情况:
情况一:插入节点Z的父节点和其叔父节点Y均为红色
对于这种情况,我们要做的操作有:将当前节点N(4)的父节点P(5)和叔叔节点U(8)涂黑,将祖父节点G(7)涂红,变成上右图所示的情况。再将当前节点指向其祖父节点,再次从新的当前节点开始算法
情况二:由于第一种情况将父节点和叔节点都涂黑了,祖父节点设为当前节点N(7),如果当前节点的(此时为N(7))的父节点仍然为红色,叔父节点为黑色,且当前节点是其父节点的右子节点
我们要做的操作有:将当前节点(7)的父节点(2)作为新的节点,以新的当前节点为支点做左旋操作。
情况三:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点。
将当前节点的父节点(7)涂黑,将祖父节点(11)涂红,以祖父节点为支点做右旋操作。最后把根节点涂黑
三种情况的关系:
情况1与情况2、情况3的区别在于插入节点的父节点的兄弟节点颜色不同,对于实际情况的插入操作:
(1)如果遇到情况1,则调整过程可能必然会经历情况2和3
(2) 如果遇到情况2,则调整过程必然会经历情况3
(3) 如果遇到情况3,则只需按照情况三的应对措施调整即可
所以整个迭代过程将直至当前节点Z的父节点为黑时,终止。
红黑树插入操作代码示例:
void rbtree_insert(RBTRoot *root, Node *node){
Node *y = NULL;
Node *x = root->node;
// 利用平衡二叉树的插入思想,插入节点Node
while(x != NULL){
y = x;
// 如果小于当前节点,则进入左子树插入,否则进入右子树
if(node->key < x->key){
x = x->left;
}else{
x = x->right;
}
}
// 循环退出时,找到了插入节点的父节点y
node->parent = y;
if(y != NULL){
// 如果比父节点y的key小,设置为左子节点,否则设置其为y的右子节点
if(node->key < y->key){
y->left = node;
}else{
y->right = node;
}
}
// 如果y为空,则说明RB树中没有节点,将node设置为根节点
else{
root->node = node;
}
// 设置插入节点的颜色为红色
node->color = RED;
// 将RB树从新进行调整
rbtree_insert_fixup(root, node);
}
void rbtree_insert_fixup(RBTRoot *root, Node *node){
// 如果当前节点的父节点存在,并且为红色,则一直迭代调整
while(node->parent != NULL && node->parent->color == RED){
Node *pNode = node->parent;
// 如果父节点是其祖父节点的左子树
if(pNode == pNode->parent->left){
Node *y = pNode->parent->right;
// 第一种情况:插入节点node的父节点和其叔父节点Y均为红色
if(y != NULL && y->color == RED){
pNode->color = BLACK; //将父节点涂黑
y->color = BLACK; //将叔父节点涂黑
pNode->parent->color = RED; // 祖父节点涂红
node = pNode->parent; // 将当前节点设置为祖父节点
continue;
}
// 第二种情况: 父节点为红色,叔父节点为黑色,且当前节点是其父节点的右子节点
else if(node == pNode->right) {
Node *tmp;
rbtree_left_rotate(root,pNode); // 进行左旋操作
tmp = pNode;
pNode = node;
node = tmp;
}
// 第三种情况:插入节点的父节点是红色,叔叔节点是黑色,且插入节点是其父节点的左子节点
pNode->color = BLACK;
pNode->parent->color = RED;
rbtree_right_rotate(root,pNode->parent);
}
//如果父节点是其祖父节点的右子树
else{
Node *y = pNode->parent->left;
if(y != NULL && y->color == RED){
y->color = BLACK; // 叔父节点涂黑
pNode->color = BLACK; //将父节点涂黑
pNode->parent->color = RED; // 祖父节点涂红
node = pNode->parent; // 将当前节点设置为祖父节点
continue;
}
// 第二种情况:
else if(pNode->left == node){
Node *tmp;
rbtree_right_rotate(root,pNode);
tmp = pNode;
pNode = node;
node = tmp;
}
// 第三种情况:
pNode->color = BLACK;
pNode->parent->color = RED;
rbtree_left_rotate(root,pNode->parent);
}
}
}
五、删除操作
红黑树的删除操作类似于平衡二叉树的删除操作,分为删除和调整两个步骤,
删除策略:首先通过搜索找到待删除的节点node,
1) 如果节点node没有子节点,那么直接删除该节点即可
2) 如果节点node只有一个孩子节点,则直接将node的key值设置为孩子节点的key,删除node的孩子节点
3) 如果节点node有两个非空子节点,则找到节点node的后继节点X,然后把node的key替换为X的key,删除后继X
调整策略:
由上面可知,删除某个节点后,会用它的后继节点来填上,并且后继节点会设置为和删除节点同样的颜色,所以删除节点的那个位置是不会破坏平衡的。可能破坏平衡的是后继节点原来的位置,因为后继节点拿走了,原来的位置结构改变了,这就会导致不平衡的出现,需要调整;
假设要删除的节点记为node
要删除节点的后继记为next
后继next的右孩子记为rchild(next肯定不存在左孩子,因为在查找node的后继时一直向左走的)
分析:
1) 如果next节点为红色,那么不管之前要删除的node节点是红还是黑,也就是说node替换为next后,真正要删除的节点是next,而next又是红色,所以不影响红黑树的性质。
2) 如果next节点为黑色,则分多种情况讨论:
a) 如果next的右孩子rchild存在,并且是颜色是红色,则直接将rchild涂黑,代替next即可,不违背红黑树性质(理解:因为next替代了node,next这条分枝上少了黑色节点,而rchild正好又是红色的,涂黑,相当于补回了这个黑色节点)
b) 如果next的右孩子rchild存在,并且是颜色是黑色的,这个时候就会引起红黑树的不平衡(相当于next节点去接替node, next这条路上很有可能少了黑节点,所以要调整,这只是我的直观的理解,也可能不正确,毕竟功力尚浅)因此会存在四种情况需要调整,放在第三点单独讨论;
具体调整:
由以上分析可知,当后继节点是黑色,且后继节点的孩子节点也是黑色时,这个时候会发生红黑树的性质违背,因此要进行调整,具体的情况讨论如下:
在讨论之前我们先记当前节点为后继节点的右孩子节点,也就是分析过程中的rchild节点,此时rchild已经放在了next节点位置
情况一:当前节点是黑色的,且兄弟节点是红色的
说明:A节点表示当前节点。针对这种情况,我们要做的操作有:将父节点(B)涂红,将兄弟节点(D)涂黑,然后将当前节点(A)的父节点(B)作为支点左旋,然后当前节点的兄弟节点就变成黑色的情况了,也就是属于情况2或3或4
情况二:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的两个子节点均为黑色的。
A表示当前节点。针对这种情况,我们要做的操作有:将兄弟节点(D)涂红,将当前节点指向其父节点(B),因为原来要删除的节点为next(黑色,现在A顶替了next),使得A子树相对于其兄弟D子树少一个黑色节点,可以将D置为红色,这样,A子树与D子树黑色节点一致,保持了平衡。但是通过B节点的路径比不通过B节点的路径少了一个黑色节点,因此再以B节点为当前节点进行调整;
情况三:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的左子节点是红色,右子节点时黑色的。
说明:
A是当前节点。针对这种情况,我们要做的操作有:把当前节点的兄弟节点(D)涂红,把兄弟节点的左子节点(C)涂黑,然后以兄弟节点作为支点做右旋操作。然后兄弟节点就变成黑色的,经过A和C的所有路径仍有同样数目的黑色节点,且兄弟节点的右子节点变成红色的情况,也就是转换到了情况四;
情况四:当前节点是黑色的,且兄弟节点是黑色的,且兄弟节点的右子节点是红色,左子节点任意颜色
A为当前节点,针对这种情况,我们要做的操作有:交换父节点B与兄弟节点D颜色,把兄弟节点的右子节点(E)涂黑,然后以当前节点的父节点为支点做左旋操作;因此通过 N 的路径都增加了一个黑色节点。
删除操作代码示例:
void rbtree_delete(RBTRoot *root, Type key){
Node *node = rbtree_search(root,key);
if(node == NULL) return;
Node *child, *parent;
if(node->left != NULL && node->right != NULL){
// 查找后继节点
Node *replace = node;
replace = node->right;
while(replace->left != NULL){
replace = replace->left;
}
// 如果 node的父节点存在
if(node->parent != NULL){
if(node == node->parent->left){
node->parent->left = replace;
}else{
node->parent->right = replace;
}
}
// 如果父节点不存在,更新根节点
else{
root->node = replace;
}
// 后继节点的右孩子,也就是要顶替后继节点,作为调整的目标节点
child = replace->right;
parent = replace->parent;
// 记录后继节点的颜色
Color color = replace->color;
if(parent == node){
parent = replace;
} else{
if(child != NULL){
child->parent = parent;
}
parent->left = child;
replace->right = node->right;
node->right->parent = replace;
}
replace->parent = node->parent;
replace->color = node->color;
replace->left = node->left;
node->left->parent = replace;
// 如果删除的节点的后继节点是黑色,则需要调整
if(color == BLACK){
rbtree_delete_fixup(root,child,parent);
}
free(node);
return;
}
}
删除之后的调整部分代码以及运行测试部分,未完待续......
本文深入探讨红黑树的性质、左右旋转、插入及删除操作,通过实例解析算法原理。
665

被折叠的 条评论
为什么被折叠?



