红黑树的实现

1、红黑树原理

为了避免二叉搜索树出现退化为链表的情况,出现了自平衡的二叉搜索树。
AVL树:平衡二叉树,它的左右子树高度之差不超过1,不过它太过于理想化。
通过性能综合考虑选用红黑树。

1、红黑树性质

1、每个结点不是红色就是黑色
2、不可能有连在一起的红色结点
3、根结点都是黑色
4、每个红色结点的两个子结点都是黑色。
5、叶子结点都是黑色(叶子结点指的是NULL结点)
6、 从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

2、变换规则(从插入结点的角度来讲)

变换规则分为三种:

变色、左旋、右旋

1.变色

变色的前提:当前结点的父结点是红色,且它的祖父结点的另一个结点==(叔结点)也是红色==。
变色的过程:

1、将父结点设为黑色
2、叔结点设为黑色
3、祖父结点设为红色
4、把指针定义到祖父结点设为当前要操作的结点。
5、继续分析判断

示例如下:
按照二叉搜索树的规则插入对应的结点6;发现违反规则了,两个红色的连到一起了,并且符合变色的前提,所以进行变色

图1 插入结点6,将其作为当前结点
图2 做完变色变换后,将之前的祖父结点作为当前结点

2.左旋

观察变色之后的树,发现仍然是违反规则的:5、12两个红色连在一起了。
然后再看是不是符合变色的前提:显然不符合,因为12的叔结点30不是红色。那么这时就应该判断是否符合左旋的前提了。
左旋的前提:
当前父结点是红色,叔叔是黑色的时候,且当前的结点是右子树。
左旋的过程:

以父结点作为中心旋转,动态图如下:
1、将当前以当前结点为根结点的子树(between E and S)连接到祖父结点E
2、E和S中较大的结点作为移动到根结点
在这里插入图片描述

左旋前后对比:

图1 左旋前
图2 左旋后

3.右旋

观察左旋之后的树,发现仍然是违反规则的:5、12两个红色的结点连在一起。且不符合变色和左旋的条件。
那么接下来就要使用右旋
右旋条件:
当前父结点是红色,叔结点是黑色,且当前的结点是左子树。
右旋的操作:

1、把父结点变为黑色
2、把祖父结点变为红色
3、以祖父结点旋转
在这里插入图片描述

右旋前后对比;13重新连接。

图1 右旋前
图2 右旋后

3、删除结点需要注意的地方

1、将红黑树当做一棵二叉搜索树,将该结点从二叉搜索树中删除
2、通过旋转和变色操作来修正这棵树,使之重新成为一棵红黑树
关于二叉搜索树的删除结点不是这篇笔记的重点,可以参考我之前的一个刷题笔记,还是比较有用处的:

二叉搜索树的插入、删除、修剪、构造操作(leetcode701、450、669、108)

怕麻烦的话也可以直接看下面的截图:
在这里插入图片描述

2、代码

1、定义结点以及构造函数

每个结点有颜色和值,左右孩子以及父结点指针。
定义带参构造函数

template <class T>
class RBTNode{
    public:
        RBTColor color;    // 颜色
        T key;            // 关键字(键值)
        RBTNode *left;    // 左孩子
        RBTNode *right;    // 右孩子
        RBTNode *parent; // 父结点

        RBTNode(T value, RBTColor c, RBTNode *p, RBTNode *l, RBTNode *r):
            key(value),color(c),parent(),left(l),right(r) {}
};

2、定义红黑树类以及声明它的方法

每棵树有一个根结点还有空结点

template <class T>
class RBTree {
    private:
        RBTNode<T> *mRoot;    // 根结点

    public:
        RBTree();
        ~RBTree();

        // 前序遍历"红黑树"
        void preOrder();
        // 中序遍历"红黑树"
        void inOrder();
        // 后序遍历"红黑树"
        void postOrder();

        // (递归实现)查找"红黑树"中键值为key的节点
        RBTNode<T>* search(T key);
        // (非递归实现)查找"红黑树"中键值为key的节点
        RBTNode<T>* iterativeSearch(T key);

        // 查找最小结点:返回最小结点的键值。
        T minimum();
        // 查找最大结点:返回最大结点的键值。
        T maximum();

        // 找结点(x)的后继结点。即,查找"红黑树中数据值大于该结点"的"最小结点"。
        RBTNode<T>* successor(RBTNode<T> *x);
        // 找结点(x)的前驱结点。即,查找"红黑树中数据值小于该结点"的"最大结点"。
        RBTNode<T>* predecessor(RBTNode<T> *x);

        // 将结点(key为节点键值)插入到红黑树中
        void insert(T key);

        // 删除结点(key为节点键值)
        void remove(T key);

        // 销毁红黑树
        void destroy();

        // 打印红黑树
        void print();
    private:
        // 前序遍历"红黑树"
        void preOrder(RBTNode<T>* tree) const;
        // 中序遍历"红黑树"
        void inOrder(RBTNode<T>* tree) const;
        // 后序遍历"红黑树"
        void postOrder(RBTNode<T>* tree) const;

        // (递归实现)查找"红黑树x"中键值为key的节点
        RBTNode<T>* search(RBTNode<T>* x, T key) const;
        // (非递归实现)查找"红黑树x"中键值为key的节点
        RBTNode<T>* iterativeSearch(RBTNode<T>* x, T key) const;

        // 查找最小结点:返回tree为根结点的红黑树的最小结点。
        RBTNode<T>* minimum(RBTNode<T>* tree);
        // 查找最大结点:返回tree为根结点的红黑树的最大结点。
        RBTNode<T>* maximum(RBTNode<T>* tree);

        // 左旋
        void leftRotate(RBTNode<T>* &root, RBTNode<T>* x);
        // 右旋
        void rightRotate(RBTNode<T>* &root, RBTNode<T>* y);
        // 插入函数
        void insert(RBTNode<T>* &root, RBTNode<T>* node);
        // 插入修正函数
        void insertFixUp(RBTNode<T>* &root, RBTNode<T>* node);
        // 删除函数
        void remove(RBTNode<T>* &root, RBTNode<T> *node);
        // 删除修正函数
        void removeFixUp(RBTNode<T>* &root, RBTNode<T> *node, RBTNode<T> *parent);

        // 销毁红黑树
        void destroy(RBTNode<T>* &tree);

        // 打印红黑树
        void print(RBTNode<T>* tree, T key, int direction);

#define rb_parent(r)   ((r)->parent)
#define rb_color(r) ((r)->color)
#define rb_is_red(r)   ((r)->color==RED)
#define rb_is_black(r)  ((r)->color==BLACK)
#define rb_set_black(r)  do { (r)->color = BLACK; } while (0)
#define rb_set_red(r)  do { (r)->color = RED; } while (0)
#define rb_set_parent(r,p)  do { (r)->parent = (p); } while (0)
#define rb_set_color(r,c)  do { (r)->color = (c); } while (0)
};

3、左旋

/* 
 * 对红黑树的节点(x)进行左旋转
 *
 * 左旋示意图(对节点x进行左旋):
 *      px                              px
 *     /                               /
 *    x                               y                
 *   /  \      --(左旋)-->           / \                #
 *  lx   y                          x  ry     
 *     /   \                       /  \
 *    ly   ry                     lx  ly  
 *
 *
 */
template <class T>
void RBTree<T>::leftRotate(RBTNode<T>* &root, RBTNode<T>* x)
{
    // 设置x的右孩子为y
    RBTNode<T> *y = x->right;

    // 将 “y的左孩子” 设为 “x的右孩子”;
    // 如果y的左孩子非空,将 “x” 设为 “y的左孩子的父亲”
    x->right = y->left;
    if (y->left != NULL)
        y->left->parent = x;

    // 将 “x的父亲” 设为 “y的父亲”
    y->parent = x->parent;

    if (x->parent == NULL)
    {
        root = y;            // 如果 “x的父亲” 是空节点,则将y设为根节点
    }
    else
    {
        if (x->parent->left == x)
            x->parent->left = y;    // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
        else
            x->parent->right = y;    // 如果 x是它父节点的左孩子,则将y设为“x的父节点的左孩子”
    }
    
    // 将 “x” 设为 “y的左孩子”
    y->left = x;
    // 将 “x的父节点” 设为 “y”
    x->parent = y;
}

4、右旋

/* 
 * 对红黑树的节点(y)进行右旋转
 *
 * 右旋示意图(对节点y进行左旋):
 *            py                               py
 *           /                                /
 *          y                                x                  
 *         /  \      --(右旋)-->            /  \                     #
 *        x   ry                           lx   y  
 *       / \                                   / \                   #
 *      lx  rx                                rx  ry
 * 
 */
template <class T>
void RBTree<T>::rightRotate(RBTNode<T>* &root, RBTNode<T>* y)
{
    // 设置x是当前节点的左孩子。
    RBTNode<T> *x = y->left;

    // 将 “x的右孩子” 设为 “y的左孩子”;
    // 如果"x的右孩子"不为空的话,将 “y” 设为 “x的右孩子的父亲”
    y->left = x->right;
    if (x->right != NULL)
        x->right->parent = y;

    // 将 “y的父亲” 设为 “x的父亲”
    x->parent = y->parent;

    if (y->parent == NULL) 
    {
        root = x;            // 如果 “y的父亲” 是空节点,则将x设为根节点
    }
    else
    {
        if (y == y->parent->right)
            y->parent->right = x;    // 如果 y是它父节点的右孩子,则将x设为“y的父节点的右孩子”
        else
            y->parent->left = x;    // (y是它父节点的左孩子) 将x设为“x的父节点的左孩子”
    }

    // 将 “y” 设为 “x的右孩子”
    x->right = y;

    // 将 “y的父节点” 设为 “x”
    y->parent = x;
}

5、插入操作

内部接口 – insert(root, node)的作用是将"node"节点插入到红黑树中。其中,root是根,node是被插入节点。
外部接口 – insert(key)的作用是将"key"添加到红黑树中。

/* 
 * 将结点插入到红黑树中
 *
 * 参数说明:
 *     root 红黑树的根结点
 *     node 插入的结点        // 对应《算法导论》中的node
 */
template <class T>
void RBTree<T>::insert(RBTNode<T>* &root, RBTNode<T>* node)
{
    RBTNode<T> *y = NULL;
    RBTNode<T> *x = root;

    // 1. 将红黑树当作一颗二叉查找树,将节点添加到二叉查找树中。
    while (x != NULL)
    {
        y = x;
        if (node->key < x->key)
            x = x->left;
        else
            x = x->right;
    }

    node->parent = y;
    if (y!=NULL)
    {
        if (node->key < y->key)
            y->left = node;
        else
            y->right = node;
    }
    else
        root = node;

    // 2. 设置节点的颜色为红色
    node->color = RED;

    // 3. 将它重新修正为一颗二叉查找树
    insertFixUp(root, node);
}

/* 
 * 将结点(key为节点键值)插入到红黑树中
 *
 * 参数说明:
 *     tree 红黑树的根结点
 *     key 插入结点的键值
 */
template <class T>
void RBTree<T>::insert(T key)
{
    RBTNode<T> *z=NULL;

    // 如果新建结点失败,则返回。
    if ((z=new RBTNode<T>(key,BLACK,NULL,NULL,NULL)) == NULL)
        return ;

    insert(mRoot, z);
}

6、修正操作

根据条件,分别进行变色、左旋、右旋操作,最后再将根结点置为黑色。
对于叔叔结点是组父结点的左孩子还是右孩子需要分类讨论。
操作的具体步骤,可以返回上文对比看。
insertFixUp(root, node)的作用是对应"上面所讲的第三步"。它是一个内部接口。

/*
 * 红黑树插入修正函数
 *
 * 在向红黑树中插入节点之后(失去平衡),再调用该函数;
 * 目的是将它重新塑造成一颗红黑树。
 *
 * 参数说明:
 *     root 红黑树的根
 *     node 插入的结点        // 对应《算法导论》中的z
 */
template <class T>
void RBTree<T>::insertFixUp(RBTNode<T>* &root, RBTNode<T>* node)
{
    RBTNode<T> *parent, *gparent;

    // 若“父节点存在,并且父节点的颜色是红色”
    while ((parent = rb_parent(node)) && rb_is_red(parent))
    {
        gparent = rb_parent(parent);
        //若“父节点”是“祖父节点的左孩子”
        if (parent == gparent->left)
        {
        	RBTNode<T> *uncle = gparent->right;
            // Case 1条件:叔叔节点是红色,此时采用变色
             if (uncle && rb_is_red(uncle))
             {
                 rb_set_black(uncle);
                 rb_set_black(parent);
                 rb_set_red(gparent);
                 node = gparent;
                 continue;
             }
            // Case 2条件:叔叔是黑色,且当前节点是右孩子,此时采用左旋
            if (parent->right == node)
            {
                RBTNode<T> *tmp;
                leftRotate(root, parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3条件:叔叔是黑色,且当前节点是左孩子,此时采用右旋
            rb_set_black(parent);
            rb_set_red(gparent);
            rightRotate(root, gparent);
        } 
        else//若“z的父节点”是“z的祖父节点的右孩子”
        {
             RBTNode<T> *uncle = gparent->left;
             // Case 1条件:叔叔节点是红色
             if (uncle && rb_is_red(uncle))
             {
                 rb_set_black(uncle);
                 rb_set_black(parent);
                 rb_set_red(gparent);
                 node = gparent;
                 continue;
             }
            // Case 2条件:叔叔是黑色,且当前节点是左孩子
            if (parent->left == node)
            {
                RBTNode<T> *tmp;
                rightRotate(root, parent);
                tmp = parent;
                parent = node;
                node = tmp;
            }

            // Case 3条件:叔叔是黑色,且当前节点是右孩子。
            rb_set_black(parent);
            rb_set_red(gparent);
            leftRotate(root, gparent);
        }
    }
    // 将根节点设为黑色
    rb_set_black(root);
}

7、删除操作

将红黑树内的某一个节点删除。需要执行的操作依次是:首先,将红黑树当作一颗二叉查找树,将该节点从二叉查找树中删除;然后,通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。

3、参考链接

1、红黑树 - C++代码实现
2、leetcode刷题(十):树(红黑树,B树)
3、B站视频,关于红黑树的推导
4、慕课视频,关于模板类的写法指导
5、二叉搜索树的插入、删除、修剪、构造操作(leetcode701、450、669、108)
6、红黑树的删除调整过程(转载)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾牙慧者

欢迎请作者喝奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值