算法导论书上给出的红黑树的性质如下,跟STL源码剖析书上面的4条性质大同小异。
1、每个结点或是红色的,或是黑色的
2、根节点是黑色的
3、每个叶结点(NIL)是黑色的
4、如果一个节点是红色的,则它的两个儿子都是黑色的。
5、对于每个结点,从该结点到其子孙结点的所有路径上包含相同数目的黑色结点。
从红黑树上删除一个节点,可以先用普通二叉搜索树的方法,将节点从红黑树上删除掉,然后再将被破坏的红黑性质进行恢复。
我们回忆一下普通二叉树的节点删除方法:Z指向需要删除的节点,Y指向实质结构上被删除的结点,如果Z节点只有一个子节点或没有子节点,那么Y就是指向Z指向的节点。如果Z节点有两个子节点,那么Y指向Z节点的后继节点(其实前趋也是一样的),而Z的后继节点绝对不可能有左子树。因此,仅从结构来看,二叉树上实质被删除的节点最多只可能有一个子树。
现在我们来看红黑性质的恢复过程:
如果Y指向的节点是个红色节点,那么直接删除掉Y以后,红黑性质不会被破坏。操作结束。
如果Y指向的节点是个黑色节点,那么就有几条红黑性质可能受到破坏了。首先是包含Y节点的所有路径,黑高度都减少了一(第5条被破坏)。其次,如果Y的有红色子节点,Y又有红色的父节点,那么Y被删除后,就出现了两个相邻的红色节点(第4条被破坏)。最后,如果Y指向的是根节点,而Y的子节点又是红色的,那么Y被删除后,根节点就变成红色的了(第2条被破坏)。
其中,第5条被破坏是让我们比较难受的。因为这影响到了全局。这样动作就太大太复杂了。而且在这个条件下,进行其它红黑性质的恢复也很困难。所以我们首先解决这个问题:如果不改变含Y路径的黑高度,那么树的其它部分的黑高度就必须做出相应的变化来适应它。所以,我们想办法恢复原来含Y节点的路径的黑高度。做法就是:无条件的把Y节点的黑色,推到它的子节点X上去。(X可能是NIL节点)。这样,X就可能具有双重黑色,或同时具有红黑两色,也就是第1条性质被破坏了。
但第1条性质是比较容易恢复的:一、如果X是同时具有红黑两色,那么好办,直接把X涂成黑色,就行了。而且这样把所有问题都解决了。因为将X变为黑色,2、4两条如果有问题的话也会得到恢复,算法结束。二、如果X是双黑色,那么我们希望把这种情况向上推一直推到根节点(调整树结构和颜色,X的指向新的双黑色节点,X不断向上移动),让根节点具双黑色,这时,直接把X的一层黑色去掉就行了(因为根节点被包含在所有的路径上,所以这样做所有路径同时黑高减少一,不会破坏红黑特征)。
下面就具体地分析如何恢复1、2、4三个可能被破坏的红黑特性:我们知道,如果X指向的节点是有红黑两色,或是X是根节点时,只需要简单的对X进行一些改变就行了。要对除X节点外的其它节点进行操作时,必定是这样的情况:X节点是双层黑色,且X有父节点P。由知可知,X必然有兄弟节点W,而且这个W节点必定有两个子节点。(因为这是原树满足红黑条件要求而自然具备的。X为双黑色,那么P的另一个子节点以下一定要有至少两层的节点,否则黑色高度不可能和X路径一致)。所以我们就分析这些节点之间如何变形,把问题限制在比较小的范围内解决。另一个前提是:X在一开始,肯定是树底的叶节点或是NIL节点,所以在递归向上的过程中,每一步都保证下一步进行时,至少
X的子树是满足红黑特性的。因此子树的情况就可以认为是已经正确的了,这样,分析就只限制在X节点,X的父节点P和X的兄弟节点W,以及W的两个子节点。这些个节点中。
下面仅仅考虑X原本是黑色的情况即可。
在这种情况下,X此时应该具有双重黑色,算法的过程就是将这多出的一重黑色向上移动,直到遇到红节点或者根节点。
接着往下分析, 会遇到4种情况,实际上是8种, 因为其中4种是相互对称的,这可以通过判断X是其父节点的右孩子还是左孩子来区分。下面我们以X是其父节点的左孩子的情况来分析这4种情况,实际上接下来的调整过程,就是要想方设法将经过X的所有路径上的黑色节点个数增加1。
具体分为以下四种情况:(下面针对x是左儿子的情况讨论,右儿子对称)
Case1:X的兄弟W是红色(想办法将其变为黑色)
由于W是红色的,因此其儿子节点和父节点必为黑色,只要将W和其父节点的颜色对换,在对父节点进行一次左旋转,便将W的左子节点放到了X的兄弟节点上,X的兄弟节点变成了黑色,且红黑性质不变。但还不算完,只是暂时将情况1转变成了下面的情况2或3或4。
Case2:X的兄弟节点W是黑色的,而且W的两个子节点都是黑色的。此时可以将X的一重黑色和W的黑色同时去掉,而转加给他们的父节点上,这是X就指向它的父节点了,因此此时父节点具有双重颜色了。这一重黑色节点上移。
如果父节点原来是红色的,现在又加一层黑色,那么X现在指向的这个节点就是红黑两色的,直接把X(也就是父节点)着为黑色。问题就已经完整解决了。
如果父节点现在是双层黑色,那就以父节点为新的X进行向上的下一次的递归。
Case3:X的兄弟节点W是黑色的,而且W的左子节点是红色的,右子节点是黑色的。此时通过交换W和其左子节点的颜色并进行一次向右旋转就可转换成下面的第四种情况。注意,原来L是红色的,所以L的子节点一定是黑色的,所以旋转中L节点的一个子树挂到之后着为红色的W节点上不会破坏红黑性质。变形后黑色高度不变。
Case4:X的兄弟节点W是黑色的,而且W的右子节点是红色的。这种情况下,做一次左旋,W就处于根的位置,将W保持为原来的根的位置的颜色,同时将W的两个新的儿子节点的颜色变为黑色,去掉X的一重黑色。这样整个问题也就得到了解决。递归结束。(在代码上,为了标识递归结束,我们把X指向根节点)
因此,只要按上面四种情况一直递归处理下去,X最终总会指向根结点或一个红色结点,这时我们就可以结束递归并把问题解决了。
以上就是红黑树的节点删除全过程。
总结:
如果我们通过上面的情况画出所有的分支图,我们可以得出如下结论
插入操作:解决的是 红-红 问题
删除操作:解决的是 黑-黑 问题
即你可以从分支图中看出,需要往上遍历的情况为红红(插入),或者为黑黑黑(删除)的情况,如果你认真分析并总结所有的情况后,并坚持下来,红黑树也就没有想象中的那么恐怖了,并且很美妙;
详细的红黑树删除节点的代码如下:
#include<iostream>
using namespace std;
// 定义节点颜色
enum COLOR
{
BLACK = 0,
RED
};
// 红黑树节点
typedef struct RB_Tree_Node
{
int key;
struct RB_Tree_Node *left;
struct RB_Tree_Node *right;
struct RB_Tree_Node *parent;
unsigned char RB_COLOR;
}RB_Node;
// 红黑树,包含一个指向根节点的指针
typedef struct RBTree
{
RB_Node* root;
}*RB_Tree;
// 红黑树的NIL节点
static RB_Tree_Node NIL = {0, 0, 0, 0, BLACK};
#define PNIL (&NIL) // NIL节点地址
void Init_RBTree(RB_Tree pTree) // 初始化一棵红黑树
{
pTree->root = PNIL;
}
// 查找最小键值节点
RB_Node* RBTREE_MIN(RB_Node* pRoot)
{
while (PNIL != pRoot->left)
{
pRoot = pRoot->left;
}
return pRoot;
}
/*
15
/ \
/ \
/ \
6 18
/ \ / \
/ \ / \
3 7 17 20
/ \ \
/ \ \
2 4 13
/
/
9
*/
// 查找指定节点的后继节点
RB_Node* RBTREE_SUCCESSOR(RB_Node* pRoot)
{
if (PNIL != pRoot->right) // 查找图中6的后继节点时就调用RBTREE_MIN函数
{
return RBTREE_MIN(pRoot->right);
}
// 节点没有右子树的时候,进入下面的while循环(如查找图中13的后继节点时,它的后继节点是15)
RB_Node* pParent = pRoot->parent;
while((PNIL != pParent) && (pRoot == pParent->right))
{
pRoot = pParent;
pParent = pRoot->parent;
}
return pParent;
}
// 红黑树的节点删除
RB_Node* Delete(RB_Tree pTree , RB_Node* pDel)
{
RB_Node* rel_delete_point;
if(pDel->left == PNIL || pDel->right == PNIL)
rel_delete_point = pDel;
else
rel_delete_point = RBTREE_SUCCESSOR(pDel); // 查找后继节点
RB_Node* delete_point_child;
if(rel_delete_point->right != PNIL)
{
delete_point_child = rel_delete_point->right;
}
else if(rel_delete_point->left != PNIL)
{
delete_point_child = rel_delete_point->left;
}
else
{
delete_point_child = PNIL;
}
delete_point_child->parent = rel_delete_point->parent;
if(rel_delete_point->parent == PNIL) // 删除的节点是根节点
{
pTree->root = delete_point_child;
}
else if(rel_delete_point == rel_delete_point->parent->right)
{
rel_delete_point->parent->right = delete_point_child;
}
else
{
rel_delete_point->parent->left = delete_point_child;
}
if(pDel != rel_delete_point)
{
pDel->key = rel_delete_point->key;
}
if(rel_delete_point->RB_COLOR == BLACK)
{
DeleteFixUp(pTree , delete_point_child);
}
return rel_delete_point;
}
/*
算法导论上的描述如下:
RB-DELETE-FIXUP(T, x)
1 while x ≠ root[T] and color[x] = BLACK
2 do if x = left[p[x]]
3 then w ← right[p[x]]
4 if color[w] = RED
5 then color[w] ← BLACK Case 1
6 color[p[x]] ← RED Case 1
7 LEFT-ROTATE(T, p[x]) Case 1
8 w ← right[p[x]] Case 1
9 if color[left[w]] = BLACK and color[right[w]] = BLACK
10 then color[w] ← RED Case 2
11 x p[x] Case 2
12 else if color[right[w]] = BLACK
13 then color[left[w]] ← BLACK Case 3
14 color[w] ← RED Case 3
15 RIGHT-ROTATE(T, w) Case 3
16 w ← right[p[x]] Case 3
17 color[w] ← color[p[x]] Case 4
18 color[p[x]] ← BLACK Case 4
19 color[right[w]] ← BLACK Case 4
20 LEFT-ROTATE(T, p[x]) Case 4
21 x ← root[T] Case 4
22 else (same as then clause with "right" and "left" exchanged)
23 color[x] ← BLACK
*/
//接下来的工作,很简单,即把上述伪代码改写成c++代码即可
void DeleteFixUp(RB_Tree pTree , RB_Node* node)
{
while(node != pTree->root && node->RB_COLOR == BLACK)
{
if(node == node->parent->left)
{
RB_Node* brother = node->parent->right;
if(brother->RB_COLOR==RED) //情况1:x的兄弟w是红色的。
{
brother->RB_COLOR = BLACK;
node->parent->RB_COLOR = RED;
RotateLeft(node->parent);
}
else //情况2:x的兄弟w是黑色的,
{
if(brother->left->RB_COLOR == BLACK && brother->right->RB_COLOR == BLACK) //w的两个孩子都是黑色的
{
brother->RB_COLOR = RED;
node = node->parent;
}
else
{
if(brother->right->RB_COLOR == BLACK) //情况3:x的兄弟w是黑色的,w的右孩子是黑色(w的左孩子是红色)。
{
brother->RB_COLOR = RED;
brother->left->RB_COLOR = BLACK;
RotateRight(brother);
brother = node->parent->right; //情况3转换为情况4
}
//情况4:x的兄弟w是黑色的,且w的右孩子时红色的
brother->RB_COLOR = node->parent->RB_COLOR;
node->parent->RB_COLOR = BLACK;
brother->right->RB_COLOR = BLACK;
RotateLeft(node->parent);
node = pTree->root;
}//else
}//else
}
else //同上,原理一致,只是遇到左旋改为右旋,遇到右旋改为左旋即可。其它代码不变。
{
RB_Node* brother = node->parent->left;
if(brother->RB_COLOR == RED)
{
brother->RB_COLOR = BLACK;
node->parent->RB_COLOR = RED;
RotateRight(node->parent);
}
else
{
if(brother->left->RB_COLOR==BLACK && brother->right->RB_COLOR == BLACK)
{
brother->RB_COLOR = RED;
node = node->parent;
}
else
{
if(brother->left->RB_COLOR==BLACK)
{
brother->RB_COLOR = RED;
brother->right->RB_COLOR = BLACK;
RotateLeft(brother);
brother = node->parent->left; //情况3转换为情况4
}
brother->RB_COLOR = node->parent->RB_COLOR;
node->parent->RB_COLOR = BLACK;
brother->left->RB_COLOR = BLACK;
RotateRight(node->parent);
node = pTree->root;
}
}
}
}//while
node->RB_COLOR = BLACK; //如果X节点原来为红色,那么直接改为黑色
}
分享到:
相关推荐
根据提供的文件信息,“STL源码剖析简体中文完整版(带目录)”这一标题与描述表明这是一份针对STL(Standard Template Library,标准模板库)的深入分析文档,主要面向的是希望深入了解STL内部实现原理及代码结构的...
### STL源码剖析知识点 #### 一、STL简介与重要性 标准模板库(Standard Template Library,简称STL)是C++标准库的一部分,它为C++编程提供了一组通用算法以及容器类。STL的设计哲学是将数据结构与算法分离,这样...
《STL源码剖析》是一本深入探讨标准模板库(Standard Template Library,简称STL)的书籍,主要针对C++编程语言。STL是C++编程中的一个重要组成部分,它提供了高效且灵活的数据结构和算法,极大地提升了代码的可读性...
在《STL源码剖析--侯捷》这本书中,作者深入解析了SGI STL的实现细节,包括容器的内存管理策略、迭代器的实现机制、算法的优化技巧以及功能对象的用法。通过阅读这本书,读者不仅可以理解STL的基本使用,还能深入...
1. 容器:STL中的容器是用于存储元素的数据结构,如vector(动态数组)、list(双向链表)、deque(双端队列)和set(红黑树实现的集合)。书中详细解释了它们的实现细节,包括内存管理、插入和删除操作的时间复杂度...
在《STL源码刨析》这本书中,作者可能详细剖析了STL的内部实现,包括容器的内存管理、迭代器的实现细节、算法的优化策略以及函数对象的使用方法。通过阅读源码,我们可以更深入地理解STL的工作原理,提升对C++模板和...
侯捷的《STL源码剖析》深入讲解了STL的实现原理和设计模式,读者可以通过学习,不仅掌握STL的使用,还能了解其内部机制,提升编程能力。虽然书中的文字是繁体,但只要耐心阅读,就能收获丰富的C++ STL知识。
- **Set/Map实现**:详细介绍set和map这两种关联式容器的工作原理,特别是它们如何利用红黑树来保证插入、查找等操作的时间复杂度为O(log n)。 - **应用场景**:列举了set和map在实际开发中的常见应用场景,并给出了...