红黑树的算法实现

在搜索树的大家族里,红黑树算是用途最广泛的一个代表了。在我们使用的C++STL库中,set、map都是以它作为底层去实现的。当然在一些其他的地方,比如Java集合中的TreeSet和TreeMap,还有Linux虚拟内存的管理,也是通过红黑树去实现的。所以,今天我们来分析一下红黑树是怎样实现的。

红黑树是一棵二叉搜索树,它在每个节点上增加了一个存储位来表示节点的颜色,可以是Red或Black。通过对任何一条从根到叶子简单 路径上的颜色来约束,红黑树保证最长路径不超过最短路径的两倍,因而近似于平衡。

所以我们就把满足下面性质的二叉搜索树称为红黑树:

1. 每个节点不是红色就是黑色
2. 根节点是黑色的
3. 如果有一个节点是红的,那么它的子节点一定是黑的(不存在连续的红节点)
4. 每条路径下存在相同数目的黑节点
5. 每个叶子(NIL)节点是黑色的(这是的叶子节点指的是为(NIL/NULL)的叶子结点)

1.插入

红黑树的插入情况是比较多的,最开始创建一个红黑树,它的根节点一定是黑,插入的第二个节点为了满足黑色节点数目相同的性质,肯定是红的,以此类推,当节点较多时我们可以将插入情况分为以下几类:

第一种:祖父节点是黑的,父亲和叔叔节点是红的,现在我们插入一个cur红节点(方框节点可存在,可不存在)

这里写图片描述

分析:这个时候,这棵红黑树已经不满足性质“不存在连续红节点”,所以这个时候需要对树进行调整。这种情况我们可以直接将父亲和叔叔节点的颜色进行改变为黑色,改变后又满足了红黑树的性质。

这里写图片描述

第二种:祖父节点是黑的,父亲节点是红的,叔叔节点不存在或者是黑色的,此时cur红节点(方框节点可存在,可不存在)

这里写图片描述

分析:同理,这种情况也不满足黑色节点数量相同这一性质。这个时候简单的改变颜色肯定不能解决问题,在这种情况下往往是因为cur子节点的调整,引起上面节点也需要调整。所以这个时候,我们需要对这棵树进行旋转。

这里写图片描述

第三种:祖父节点是黑的,父亲节点是红的,叔叔节点不存在或者是黑色的,此时cur红节点是父亲的右孩子(方框节点可存在,可不存在)

这里写图片描述

分析:现在对树做调整,可能情况会比较复杂,如果我们把cur节点变成父亲的左孩子,那剩下的问题就和第二种情况一样了。所以我们对父亲子树进行右旋即可。

这里写图片描述

    pair<Node*, bool> Insert(const K& key, const V& value)
    {
        if (_root == NULL)
        {
            _root = new Node(key, value);
            _root->_color = BLACK;
            return make_pair(_root, true);

        }

        Node* parent = NULL;
        Node* cur = _root;
        while (cur)
        {
            if (cur->_key < key)
            {
                parent = cur;
                cur = cur->_right;
            }

            else if (cur->_key > key)
            {
                parent = cur;
                cur = cur->_left;
            }

            else
                return make_pair(cur,false);

        }

        cur = new Node(key, value);
        if (parent->_key > key)
            parent->_left = cur;
        else
            parent->_right = cur;
        cur->_parent = parent;

        while (parent && parent->_color == RED)
        {
            Node* grandfather = parent->_parent;      //1.判断父亲和叔叔的关系
            if (parent == grandfather->_left)       //父亲在左,叔叔在右
            {
                Node* uncle = grandfather->_right;    //2.判断叔叔的情况
                if (uncle != NULL && uncle->_color == RED)    //a.叔叔存在且为红 -> 变色
                {
                    parent->_color = BLACK;
                    uncle->_color = BLACK;
                    grandfather->_color = RED;
                    cur = grandfather;
                    parent = cur->_parent;
                }

                else   //b.叔叔不存在或者叔叔为黑
                {
                    if (cur == parent->_right)  
                    {
                        RotateL(parent);
                        swap(cur, parent);
                    }

                    RotateR(grandfather);
                    parent->_color = BLACK;
                    grandfather->_color = RED;
                    break;
                }
            }


            else       //叔叔在左,父亲在右
            {
                Node* uncle = grandfather->_left;
                if (uncle && uncle->_color == RED)    //叔叔存在且为红 ,变色
                {
                    parent->_color = BLACK;
                    uncle->_color = BLACK;
                    grandfather->_color = RED;
                    cur = grandfather;
                    parent = cur->_parent;
                }

                else    //叔叔不存在或为黑
                {
                    if (cur == parent->_left)
                    {
                        RotateR(parent);
                        swap(cur, parent);
                    }

                    RotateL(grandfather);
                    parent->_color = BLACK;
                    grandfather->_color = RED;
                    break;
                }
            }

        }
        _root->_color = BLACK;
        return make_pair(_root, true);
    }

总结:关于红黑树的插入,我们是从插入的节点开始,依次向上调整,直到这棵树的节点符合红黑树的性质。

2.红黑树的左右旋

对于左右旋这个概念,其实在学习查找树这一个系列的时候并不陌生,很多时候树的插入问题,我们都会用到这个方法来使它达到一种平衡。
对红黑树也是如此。首先,我们借助一个动画来看看右旋是怎么个旋法。

这里写图片描述

根据这个动画再去实现,就比较容易了。代码实现如下。

void RotateR(Node* parent)    //右旋
    {
        assert(parent);
        Node* child = parent->_left;
        Node* grandfather = parent->_parent;
        Node* Rchild = child->_right;

        if (Rchild)
            Rchild->_parent =parent ;
        parent->_left = Rchild;
        parent->_parent = child;
        child->_right = parent;

        if (grandfather == NULL)
        {
            child->_parent = NULL;
            _root = child;
        }

        else
        {
            if (grandfather->_left == parent)
                grandfather->_left = child;

            else
                grandfather->_right = child;

            child->_parent = grandfather;
        }
    }

同理,左旋的实现也可以借助动画来理解。

这里写图片描述

void RotateL(Node* parent)   //左旋
    {
        assert(parent);
        Node* child = parent->_right;
        Node* Lchild = child->_left;
        Node* grandfather = parent->_parent;
        if (Lchild)
            Lchild->_parent = parent;
        parent->_right = Lchild;
        parent->_parent = child;
        child->_left = parent;

        if (grandfather == NULL)
        {
            _root = child;
            child->_parent = NULL;
        }

        else
        {
            if (parent == grandfather->_left)
                grandfather->_left = child;
            else
                grandfather->_right = child;

            child->_parent = grandfather;
        }
    }

总结:在查找树中,合理的使用左右旋会使整个问题化繁为简。

3.如何判断当前的红黑树是红黑树

在我们完成插入算法后,这个时候就有了一个新问题,我们如何判断一棵树是不是没有问题的红黑树?要解决这个问题,就得从红黑树的性质入手了。

1. 如果根节点是红的,肯定不是红黑树;
2. 如果有连续的红节点,肯定不是红黑树;
3. 如果黑色节点的路径长度不一样,肯定不是红黑树。

这三条性质就够我们去判断一个树是不是红黑树了。第一二条性质比好判断,如何实现第三个?这时候我们就需要去任意路径count一次黑色节点的路径长度,然后再用这个长度和每一条路径的长度进行比较,只要有一条路径不一样,那就可以直接得出这不是一颗红黑树了。直到判断到每一条路径的叶子节点后,它都相等,那么此时这个性质就满足了。

bool IsBalance()
    {
        if (_root == NULL) return true;
        if (_root->_color == RED) return false;

        int BlackNum = 0;    //统计一条路径的黑色节点数量
        Node* left = _root;
        while (left)
        {
            if (left->_color == BLACK)
                BlackNum++;
            left = left->_left;
        }

        int count = 0;
        return _IsBalance(_root,BlackNum,count);
    }
bool _IsBalance(Node* root, const int BlackNum, int count)
    {
        if (root == NULL)  return true;
        if (root->_color == RED && root->_parent->_color == RED)
        {
            cout << "存在连续红节点" << endl;
            return false;
        }

        if (root->_color == BLACK)
            ++count;

        if (root->_left == NULL && root->_right == NULL)
        {
            if (count != BlackNum)
            {
                cout << "黑节点数目不同" << endl;
                return false;
            }

            else
                return true;
        }
        return _IsBalance(root->_left, BlackNum, count) && _IsBalance(root->_right, BlackNum, count);
    }

其实,相对比其他查找树,红黑树的查找时间复杂度一直控制在 O(lgn),这就要求它一直处于近似平衡状态。也正是因为这一点,红黑树是一种非常高效的平衡查找树,所以在很多语言的内部都会或多或少使用它来作为底层实现。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值