红黑树的详细实现

本文详细介绍了红黑树的原理及其在二叉搜索树基础上的优化,包括节点插入、删除以及相应的旋转调整策略。红黑树通过维持黑色节点的平衡,确保高效的查找、插入和删除操作。文中提供了C++实现的代码示例,展示了如何处理插入和删除过程中可能破坏红黑树性质的情况。
摘要由CSDN通过智能技术生成

红黑树是一种二叉搜索树,它靠维持黑色节点的个数来保持平衡的。

红黑树的应用

红黑树应用较广,一般有两个方面的应用。一个是以key-val的方式,实现用key值查找value值的功能,如服务器中,通过socket查找与客户端对应的fd,以及操作系统中的内存块的查找等。另一个应用是利用了红黑树是二叉搜索树的性质,通过其中序遍历是有序的这一性质进行排序,例如定时器的实现。
当然,红黑树的应用还有很多,但是大多数都离不开上面提到的两条性质,这里不再列举。

红黑树和节点的实现

这里不再罗列红黑树的五条性质,直接贴出红黑树的定义。这里只实现了key域,val域在定义后面直接添加即可。

class rb_node
{
public:
    char color;
    rb_node *parent;
    rb_node *left;
    rb_node *right;
    int key;
};

class rb_tree
{
public:
    rb_node *nil;
    rb_node *root;
};

这里提示一点,在树这个类里,定义了叶子节点(nil)。这里可以让树中所有的空都指向这一节点。另外,类的成员方法没有贴出,除构造函数外,下面将一点一点介绍。

节点的旋转

红黑树要想调整节点,如同其他平衡树一样,有左旋和右旋的操作,只要注意好3个方向上的6个指针即可。这里为了方便,暂不引入颜色的概念。
在这里插入图片描述
这里也可以看出,左旋和右旋是一对互逆的操作。

//介绍一个小技巧,右旋,直接在左旋的基础上,把代码中的right和left互换即可
void left_rotate(rb_node *node)
{
    rb_node *y=node->right;
    rb_node *z=y->left;
    //第一个方向
    y->parent=node->parent;
    if(node->parent==nil)//此时node是根节点
    {
        root=y;
    }else if(node->parent->left==node)//node是左孩子
    {
        node->parent->left=y;
    }else//node是右孩子
    {
        node->parent->right=y;
    }
    //第二个方向
    node->parent=y;
    y->left=node;
    //第三个方向
    node->right=z;
    if(z!=nil)
    {
        z->parent=node;
    }
}
    
void right_rotate(rb_node *node)
{
    rb_node *y=node->left;
    rb_node *z=y->right;
    //第一个方向
    y->parent=node->parent;
    if(node->parent==nil)
    {
        root=y;
    }else if(node->parent->left==node)
    {
        node->parent->left=y;
    }else
    {
        node->parent->right=y;
    }
    //第二个方向
    node->parent=y;
    y->right=node;
    //第三个方向
    node->left=z;
    if(z!=nil)
    {
        z->parent=node;
    }
}

节点的插入

还是暂不引入颜色的概念。对于一棵二叉搜索树,插入节点的过程,是从根节点开始,将插入节点的key值与树上节点的key值进行对比,小就继续往左走,大就往右走,直到最后的叶子节点,就插入到那个位置。红黑树也是这样。

void _insert(rb_node *cur,rb_node *node)
{
    rb_node *temp=cur;
    while(cur!=nil)
    {
        temp=cur;
        if(node->key<cur->key)
        {
            cur=cur->left;
        }else if(node->key>cur->key)
        {
            cur=cur->right;
        }else//添加值在树中已存在
        {
            return;//这里的做法是直接返回,也可以根据需求换成其他操作,如覆盖原节点等。
        }
    }
    if(temp==nil)
    {
        node->parent=nil;
        root=node;
    }else if(node->key<temp->key)
    {
        temp->left=node;
        node->parent=temp;
    }else//这里已经排除了相等的情况
    {
        temp->right=node;
        node->parent=temp;
    }
    insert_fix(node);
}

这里对于要插入节点的key值已经在树中出现的情况,是不做任何操作,直接舍弃这个待插入节点的,当然,对于不同的需求,可以有不同的改动(如覆盖或继续添加这个节点)。
下面我们开始引入颜色的概念。

节点插入后的调整

在插入节点之前,对于整棵树而言,这个新节点是红色的好还是黑色的好?为了尽量不影响红黑树的五条性质,应该插入红色节点,因为如果插入黑色节点就会影响黑高,每次插入都会破坏第5条性质。对于插入节点是红色的情况,可能会破坏第4条性质,这时就需要根据情况调整。
首先说明一下,如果插入节点的父节点是黑色,这样插入后不会破坏红黑树的任何性质,不需要任何调整。所以,这里只有三种情况需要调整,且父节点是红色,祖父节点一定是黑色。

1.父节点红色,叔父节点红色
这种情况较为简单,直接进行颜色翻转就行,只不过要注意继续向上迭代检查一下,因为颜色翻转后,祖父节点有可能违背性质4。
在这里插入图片描述
情况二三相近,只不过在遇到情况二时把节点转成情况三。

2.父节点红色,叔父节点黑色,插入节点为右孩子
这种情况只需要进行一次左旋,就转换成了情况三。
在这里插入图片描述

3.父节点红色,叔父节点黑色,插入节点为左孩子
此时需要先将父节点和祖父节点的颜色翻转,然后还要对祖父节点进行右旋。
在这里插入图片描述
值得说明的是,情况二三只是截取了树的一部分,只看图中的节点是不满足红黑树的性质的。这里举一个例子,把情况二三中的z节点当作是叶子节点nil就能满足红黑树的性质了。

void insert_fix(rb_node *node)
{
    while(node->parent->color==RED)
    {
        rb_node *y=node->parent;
        if(y==y->parent->left)//插入节点的父结点是左孩子
        {
            rb_node *z=y->parent->right;
            //第一种情况,叔父节点也是红色
            if(z->color==RED)
            {
                //改变颜色即可
                y->color=BLACK;
                z->color=BLACK;
                y->parent->color=RED;
                node=node->parent->parent;//继续向上检查
            }else//叔父节点是黑色,这里有两种情况
            {
                if(node==y->right)//第二种情况,插入节点是右孩子
                {
                    left_rotate(y);
                    //这里是为了与下面第三种情况对接
                    node=y;
                    y=node->parent;
                }
                //第三种情况,插入节点是左孩子
                y->color=BLACK;
                y->parent->color=RED;
                right_rotate(y->parent);
            }//此时,在这棵子树顶端的节点是黑色,一定不会与其祖先颜色冲突
        }else//插入节点的父结点是右孩子,与左孩子情况一样,左右互换即可
        {
            rb_node *z=y->parent->left;
            if(z->color==RED)
            {
                y->color=BLACK;
                z->color=BLACK;
                y->parent->color=RED;
                node=node->parent->parent;
            }else
            {
                if(node==y->left)
                {
                    right_rotate(y);
                    node=y;
                    y=node->parent;
                }
                y->color=BLACK;
                y->parent->color=RED;
                left_rotate(y->parent);
            }
        }
    }
    root->color=BLACK;
}

这里区分父节点是左右孩子的意义,在于方便找到叔父节点,其余都是对称操作。
下面开始进入红黑树的删除部分。

后继节点

和二叉搜索树一样,如果待删除节点的左右孩子都存在,直接删除该节点会对该树造成较大影响。为了方便,一般是把待删除节点的后继节点(与待删除节点的值接近,也可以选择其前驱节点)的值用来覆盖这个待删除节点的值,然后再删除这个后继节点。这个后继节点就是这个待删除节点的右孩子中,最左侧的节点。

//红黑树的后继节点
rb_node *successor(rb_node *node)
{
    if(node==nil)
    {
        return nil;
    }
    //有右孩子
    if(node->right!=nil)
    {
        rb_node *p=node->right;
        while(p->left!=nil)
        {
            p=p->left;
        }
        return p;
    }
    //该节点没有右孩子,向上回溯,直到父结点为根或者是左孩子
    //这一段在删除节点中没有调用,可以忽略
    while(node->parent!=nil&&node->parent->right==node)
    {
        node=node->parent;
    }
    return node;
}

对于删除操作而言,用户给的不会是红黑树节点指针,而是对应的key值,所以我们也要有检索的方法。

rb_node *_search(rb_node *node,int key)
{
    if(node==nil)
    {
        return nil;
    }
    if(key==node->key)
    {
        return node;
    }
    if(key<node->key)
    {
        return _search(node->left,key);
    }
    return _search(node->right,key);
}

rb_node *search(int key)
{
    if(root==nil)
    {
        return nil;
    }
    rb_node *tmp=_search(root,key);
    return tmp;
}

这里使用了递归的方法,也可以使用迭代的方法。

节点的删除

经过上面的替换,现在真正要删除节点的左右孩子中,至少有一个nil,所以我们可以直接删去这个节点,然后把那个子节点与其父结点相连。值得注意的是,这里还有一个待调整节点y,用于下一步节点的调整。

rb_node *del(rb_node *node)
{
    if(node==nil)
    {
        cout<<"delete nil"<<endl;
        return nil;
    }
    rb_node *x=nil;//真正要删除的节点
    rb_node *y=nil;//旋转节点
    if(node->left==nil||node->right==nil)
    {
        x=node;
    }else
    {
        x=successor(node);
    }
    //左右孩子,一个为nil,一个实际存在
    if(x->left!=nil)
    {
        y=x->left;
    }
    if(x->right!=nil)
    {
        y=x->right;
    }
    y->parent=x->parent;
    if(x->parent==nil)
    {
        root=y;
    }else if(x->parent->left==x)
    {
        x->parent->left=y;
    }else
    {
        x->parent->right=y;
    }
    if(node!=x)
    {
        node->key=x->key;
    }
    if(x->color==BLACK)
    {
        del_fix(y);
    }
    return x;
}

节点删除后的调整

这里先想一个问题,带待删除节点是什么颜色,才会破坏红黑树的性质?如果删除了黑色节点,就会影响红黑树的黑高,势必会破坏红黑树的性质。也就是说,如果待删除节点是红色,不需要进行调整,如果是黑色,才需要调整。
这里一共有4种情况。还是和添加的情况一样,这里需要考虑待调整节点是左孩子还是右孩子的情况。因为情况相同,这里只说明待调整节点是左孩子的情况,右孩子的情况对称操作即可。

1.兄弟节点是黑色,侄子节点也都是黑色
这种情况最为简单,只需要把这个兄弟节点的颜色翻转一下就满足了黑高平衡的要求,只不过因为兄弟节点变成了红色,需要向上迭代检查一下。
在这里插入图片描述

2.兄弟节点是黑色,右侄子是红色
这里需要先继承父节点的颜色,并且改变父节点和这个红色侄子的颜色,还要对父节点进行左旋。
在这里插入图片描述

3.兄弟节点是黑色,左侄子是红色,右侄子是黑色
这种情况在完成一定操作后可以转化成情况2。
在这里插入图片描述

4.兄弟节点是红色
这里先把兄弟节点和父节点的颜色互换,让后再对父节点左旋,就可以转化成前三种情况。
在这里插入图片描述
这里可能会发现,图片中的红黑树都不满足黑高平衡的性质。注意这是删除了一个黑色节点后的结构,当然黑高不平衡。删除操作的调整,并没有写得很详细,具体的操作还是看代码。

void del_fix(rb_node *node)
{    
    while(node->parent!=nil&&node->color==BLACK)
    {
        if(node==node->parent->left)//node是左孩子
        {
            rb_node *w=node->parent->right;
            if(w->color==RED)//第四种情况
            {
                w->color=BLACK;
                node->parent->color=RED;
                left_rotate(node->parent);
                w=node->parent->right;
            }
            if(w->left->color==BLACK&&w->right->color==BLACK)//第一种情况
            {
                w->color=RED;
                node=node->parent;
            }else//不全是黑色,即2、3情况
            {
                if(w->right->color==BLACK)//第三种情况
                {
                    w->left->color=BLACK;
                    w->color=RED;
                    right_rotate(w);
                    w=node->parent->right;
                }
                //第二种情况
                w->color=node->parent->color;
                node->parent->color=BLACK;
                w->right->color=BLACK;
                left_rotate(node->parent);
                node=root;//退出循环
            }
        }else//node是右孩子
        {
            rb_node *w=node->parent->left;
            if(w->color==RED)//第四种情况
            {
                w->color=BLACK;
                node->parent->color=RED;
                right_rotate(node->parent);
                w=node->parent->left;
            }
            if(w->right->color==BLACK&&w->left->color==BLACK)//第一种情况
            {
                w->color=RED;
                node=node->parent;
            }else//不全是黑色,即2、3情况
            {
                if(w->left->color==BLACK)//第三种情况
                {
                    w->right->color=BLACK;
                    w->color=RED;
                    left_rotate(w);
                    w=node->parent->left;
                }
                //第二种情况
                w->color=node->parent->color;
                node->parent->color=BLACK;
                w->left->color=BLACK;
                right_rotate(node->parent);
                node=root;//退出循环
            }
        }
    }
    node->color=BLACK;
}

红黑树本身的知识点大概就是这些,主要是理解插入节点为什么是三种情况,删除节点为什么是四种情况。代码不需要强记,需要理解其中的原理和操作。
红黑树完成之后,也可以用对数器进行检验,将自己的红黑树与网上找的一版红黑树进行对比,如果没有不同,则说明自己的红黑树的实现没有问题,这里代码不再贴出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值