红黑树的旋转、插入和删除

红黑树定义和性质

红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须满足下面性质:

  • 性质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集合中的TreeSetTreeMap,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的兄弟结点设为“黑色”。
(02) 将x的父结点设为“红色”。
(03) 对x的父结点进行左旋。
(04) 左旋后,重新设置x的兄弟结点。

(05) 得到Case 2,进行Case 2的处理。

Case 2

兄弟结点是黑色,且两孩子都是黑色

(01) 将x的兄弟结点设为“红色”。
(02) 设置“x的父结点”为“新的x结点”。

(03) 重新进行删除结点的处理。

Case 3

兄弟是黑色,左孩子是红色,右孩子是黑色

(01) 将x兄弟结点的左孩子设为“黑色”。
(02) 将x兄弟结点设为“红色”。
(03) 对x的兄弟结点进行右旋。
(04) 右旋后,重新设置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的兄弟结点设为“黑色”。
(02) 将x的父结点设为“红色”。
(03) 对x的父结点进行右旋。
(04) 左旋后,重新设置x的兄弟结点。

(05) 得到Case 2,进行Case 2的处理。

Case 2

兄弟结点是黑色,且两孩子都是黑色

(01) 将x的兄弟结点设为“红色”。
(02) 设置“x的父结点”为“新的x结点”。

(03) 重新进行删除结点的处理。

Case 3

兄弟是黑色,左孩子是红色,右孩子是黑色

(01) 将x兄弟结点的左孩子设为“黑色”。
(02) 将x兄弟结点设为“红色”。
(03) 对x的兄弟结点进行左旋。
(04) 右旋后,重新设置x的兄弟结点。

(05) 得到Case 4,进行Case 4的处理。

Case 4

兄弟是黑色,右孩子是红色

(01) 将x父结点颜色 赋值给 x的兄弟结点。
(02) 将x父结点设为“黑色”。
(03) 将x兄弟结点的右孩子设为“黑色”。
(04) 对x的父结点进行右旋。
(05) 设置“x”为“根结点”。

这9种情况,用一张图片可表示为:




总结

        写在最后,这是我自己学习红黑树课程中,借鉴了网上的资料,作的一个总结,希望对后面学习红黑树的同学有一定的帮助。我个人结合代码画了几张图,最重要的就是插入和删除的两张,后面再写红黑树的插入和删除代码时,对着这两张图就可以了。

技术参考

  1. 视频技术参考: https://ke.qq.com/course/417774?flowToken=1041367
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值