红黑树定义和性质
红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:
- 性质1:每个节点要么是黑色,要么是红色。
- 性质2:根结点是黑色。
- 性质3:每个叶子结点(NIL)是黑色。
- 性质4:每个红色结点的两个子结点一定都是黑色(也就是说红结点不能相连)。
- 性质5:任意一结点到每个叶子结点的路径都包含数量相同的黑结点。
从性质5又可以推出:
- 性质5.1:如果一个结点存在黑子节点,那么该结点肯定有两个子结点。
注意:
(01) 性质3中的叶子结点,是指为空(NIL或null)的结点。
(02) 性质5,确保最长路径<最短路径的两倍。因而,红黑树是相对接近平衡的二叉树。
红黑树示意图如下:
红黑树保证最长路径不超过最短路径的二倍,最极端的情况就是:一边全是黑,长度为n,另一边全是黑红交替,黑点为n,红点为n-1,总长度为2n-1,相差为n-1。
红黑树最短路径与最长路示意图:
红黑树的应用
红黑树的应用比较广泛,主要是用它来存储有序的数据,它的时间复杂度是O(lgn),效率非常之高。
例如,Java集合中的TreeSet和TreeMap,C++ STL中的set、map,以及Linux虚拟内存的管理,都是通过红黑树去实现的。
为了后面讲解清晰,我们需要约定下红黑树一些节点的叫法,如下图所示。
为了方便,我们用字母表示一些结点,父结点(P),祖父结点(PP),兄弟结点(S),S的左结点(SL),S的右结点(SR)。
红黑树的基本操作(一) 左旋和右旋
- 左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变,如下图。
左旋的操作实际上就是调整3个结点:x,x的右子树,x的右子树的左子树,如下图:
void rbtree_left_rotate(rbtree* T, rbtree_node* x)
{
rbtree_node* y = x->right; //y设为x的右子树
x->right = y->left; //y的左孩子成为x的右孩子
if (y->left != T->nil) //如果y原来的左孩子不为空,把它的父节点设为x
{
y->left->parent = x;
}
//y调整为x的父亲,先把y的父亲设为x原来的父亲,
//然后再考虑3种情况:1.x为根节点,2.x为左孩子,3.x为右孩子
y->parent = x->parent;
if (x->parent == T->nil)
{
T->root = y;
}
else if (x == x->parent->left)
{
x->parent->left = y;
}
else
{
x->parent->right = y;
}
//最后y的左孩子设为x,x的父亲设为y
y->left = x;
x->parent = y;
}
- 右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。如下图:
右旋的操作和左旋基本类似,把左旋的leftt和right互换,x和y互换,即可。
void rbtree_right_romate(rbtree* T, rbtree_node* y)
{
rbtree_node* x = y->left;
y->left = x->right;
if (x->right != T->nil) {
x->right->father = y;
}
x->father = y->father;
if (y->father == T->nil) {
T->root = x;
}
else if (y == y->father->left) {
y->father->left = x;
}
else {
y->father->right = x;
}
x->right = y;
y->father = x;
}
那具体什么时候左旋呢?如下图的情况:
右旋的情况也类似,如下图:
红黑树的基本操作(二) 插入
将一个结点插入到红黑树中,需要执行哪些步骤呢?
我们先来思考一个问题:新插入的结点颜色设为红色好,还是黑色好?
显然,由于红黑树的性质5最麻烦,为了不破坏它,我们应该将新插入的结点颜色设为红色。那么接下来的问题,插入一个红结点会不会破坏红黑树的其它性质呢?是有可能破坏性质4的,比如插入结点的父结点颜色为红色。由此我们可以得出插入一个结点的基本步骤:
1、将红黑树当作一颗二叉查找树,将结点插入;
2、将结点着色为红色;
3、通过旋转和重新着色等方法来修正该树,使之重新成为一颗红黑树。
接下来,我们分析插入的可能情况,共有3种可能:
1. 被插入的结点是根结点。处理方法:直接把此结点涂为黑色。
2. 被插入的结点的父结点是黑色。处理方法:什么也不需要做。
3. 被插入的结点的父结点是红色,这里就比较麻烦了,会出现3种情况:
情景1:当前结点的父结点是红色且祖父结点的另一个子结点(叔叔结点)也是红色
处理方法:P和S变黑,PP变红,然后当前节点设为PP,继续处理。
情景2:当前结点的父结点是红色,叔叔结点是黑色,这里又有2种情况:
情景2.1 当前结点是其父结点的右子
处理方法:把P设为当前节点,然后左旋,得到情景2.2,按照2.2处理。
情景2.2 当前结点是其父结点的左子
处理方法:把P设为黑色,PP设为红色,以PP为节点进行右旋。
情景3:当前结点的父结点是红色,叔叔结点是黑色,这里也有2种情况:
情景3.1 当前结点是其父结点的左子
处理方法:把P设为当前节点,然后右旋,得到情景3.2,按照3.2处理。
情景3.2 当前结点是其父结点的右子
处理方法:把P设为黑色,PP设为红色,以PP为节点进行左旋。
看文字比较麻烦,我们直接用图片来表示:
红黑树插入节点的代码如下:
void rbtree_insert(rbtree* T, rbtree_node* z)
{
rbtree_node* x = T->root;
rbtree_node* y = T->nil; //y保存x的父节点
while (x != T->nil) //循环找到插入节点父节点y
{
y = x;
if (key_compare(z->key, x->key) < 0)
{
x = x->left;
}
else if (key_compare(z->key, x->key) > 0)
{
x = x->right;
}
else
{
return ;
}
}
z->parent = y;
if (y == T->nil) //如果是空树
{
T->root = z; //这里不用将z的颜色设为黑,
//在rbtree_insert_fixup函数的最后会设置z->color = BLACK;
}
else if (key_compare(z->key, y->key) < 0)
{
y->left = z;
}
else
{
y->right = z;
}
z->color = RED;
z->left = T->nil;
z->right = T->nil;
rbtree_insert_fixup(T, z);
}
红黑树插入结点修正的代码如下:
void rbtree_insert_fixup(rbtree* T, rbtree_node* z)
{
while (z->parent->color == RED) //如果父节点为红色,那么祖父节点必然存在
{
if (z->parent == z->parent->parent->left)
{
rbtree_node* y = z->parent->parent->right;
if (y->color == RED) // 插入修复情况1
{
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent;
}
else
{
if (z == z->parent->right) // 插入修复情况2.1
{
z = z->parent;
rbtree_left_rotate(T, z); //左旋后就变成插入修复情况2.2
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
rbtree_right_rotate(T, z->parent->parent);
}
}
else
{
rbtree_node* y = z->parent->parent->left;
if (y->color == RED) // 插入修复情况1
{
z->parent->color = BLACK;
y->color = BLACK;
z->parent->parent->color = RED;
z = z->parent->parent;
}
else
{
if (z == z->parent->left) // 插入修复情况3.1
{
z = z->parent;
rbtree_right_rotate(T, z); //右旋后就变成插入修复情况3.2
}
z->parent->color = BLACK;
z->parent->parent->color = RED;
rbtree_left_rotate(T, z->parent->parent);
}
}
}
T->root->color = BLACK;
}
红黑树的基本操作(三) 删除
删除红黑树结点的操作步骤依次是:首先,将红黑树当作一颗二叉查找树,将该结点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。详细描述如下:
第一步:将红黑树当作一颗二叉查找树,将结点删除。
这和删除常规二叉查找树中删除结点的方法是一样的,分3种情况:
① 被删除结点没有儿子,那么,直接将该结点删除就OK了。
② 被删除结点只有一个儿子。那么,直接删除该结点,并用该结点的唯一子结点顶替它的位置。
③ 被删除结点有两个儿子。那么,先找出它的后继结点;然后把“它的后继结点的内容”复制给该结点之后,删除“它的后继结点”。在这里,后继结点相当于替身,在将后继结点的内容复制给"被删除结点"之后,再将后继结点删除。这样就巧妙的将问题转换为"删除后继结点"的情况了,下面就考虑后继结点。 在"被删除结点"有两个非空子结点的情况下,它的后继结点不可能是双子非空。既然"的后继结点"不可能双子都非空,就意味着"该结点的后继结点"要么没有儿子,要么只有一个儿子。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。这里我曾经被一个问题困扰了好久,如果这个结点没有后继结点,怎么办?答案很简单,在③的情况下根本不可能,因为当前结点有2个儿子呀。
说明:① ② ③ 实际操作都可以统一看成是替换,① 是用叶子结点来替换,② 是用子结点来替换,③是用后继结点来替换,这样写代码就比较方便。
第二步:通过"旋转和重新着色"等一系列操作来修正该树,使之重新成为一棵红黑树。
因为"第一步"中删除结点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。下面我们分析需要调整的情况:
如果被替代的结点颜色是红色,那么我们就不用调整。
如果被替代的结点颜色是黑色,那么就共有8种情况需要考虑了,8种情况首先分两大类:
① 替换结点是父结点的左子结点,这里有4种情况:
现象说明 | 处理策略(x表示当前结点) | |
Case 1 | 兄弟结点是红色 | (01) 将x的兄弟结点设为“黑色”。 (05) 得到Case 2,进行Case 2的处理。 |
Case 2 | 兄弟结点是黑色,且两孩子都是黑色 | (01) 将x的兄弟结点设为“红色”。 (03) 重新进行删除结点的处理。 |
Case 3 | 兄弟是黑色,左孩子是红色,右孩子是黑色 | (01) 将x兄弟结点的左孩子设为“黑色”。 (05) 得到Case 4,进行Case 4的处理。 |
Case 4 | 兄弟是黑色,右孩子是红色 | (01) 将x父结点颜色 赋值给 x的兄弟结点。 (02) 将x父结点设为“黑色”。 (03) 将x兄弟结点的右孩子设为“黑色”。 (04) 对x的父结点进行左旋。 (05) 设置“x”为“根结点”。 |
②替换结点是父结点的右子结点,也有类似的4种情况:
现象说明 | 处理策略(x表示当前结点) | |
Case 1 | 兄弟结点是红色 | (01) 将x的兄弟结点设为“黑色”。 (05) 得到Case 2,进行Case 2的处理。 |
Case 2 | 兄弟结点是黑色,且两孩子都是黑色 | (01) 将x的兄弟结点设为“红色”。 (03) 重新进行删除结点的处理。 |
Case 3 | 兄弟是黑色,左孩子是红色,右孩子是黑色 | (01) 将x兄弟结点的左孩子设为“黑色”。 (05) 得到Case 4,进行Case 4的处理。 |
Case 4 | 兄弟是黑色,右孩子是红色 | (01) 将x父结点颜色 赋值给 x的兄弟结点。 (02) 将x父结点设为“黑色”。 (03) 将x兄弟结点的右孩子设为“黑色”。 (04) 对x的父结点进行右旋。 (05) 设置“x”为“根结点”。 |
这9种情况,用一张图片可表示为:
总结
写在最后,这是我自己学习红黑树课程中,借鉴了网上的资料,作的一个总结,希望对后面学习红黑树的同学有一定的帮助。我个人结合代码画了几张图,最重要的就是插入和删除的两张,后面再写红黑树的插入和删除代码时,对着这两张图就可以了。