【数据结构】红黑树的实现

在介绍红黑树之前,我们先来复习二叉搜索树和AVLTree。

1.二叉搜索树 -> 存储数据的同时方便进行高效搜索(之前的数据结构的搜索基本都是暴力搜索)

二叉搜索树的问题:极端情况下(比如有序的方式进行插入),二叉搜索树就会退化成单链形

式,效率变为o(N),效率低下。

2、AVL树 -> 在二叉树搜索树基础之上加了一个条件:左右子树高度差不超过1,并且左右子树

也满足次条件(也就说所有子树都满足)增删查改搜素效率非常高:0(logN)。

举个例子:如果内存足够的情况下,将所有中国人的信息放到树中,查找一个人的信息最多只需

要31次,通过这个例子可以感受到AVL树的效率,如果是暴力搜素需要14亿次。

3、红黑树 -> 本质上也是一个搜索二叉树,下面具体学习他的性质和他更优的地方。

一、红黑树的概念

1.红黑树的定义

搜索二叉树,节点中加了颜色,不是红色就是黑色,树中最长的路径不超过最短的路径的2倍。

AVL树是严格的平衡二叉搜索树;而红黑树是近似的平衡二叉搜索树。

2.红黑树的性质(5点)

08f05418a8de4c0ebd72a3d1e6fc1e5e.png

(1)每个结点不是红色就是黑色

(2)根节点是黑色的  

(3)如果一个节点是红色的,则它的两个孩子结点是黑色的  

(4)对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点  

(5)每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

简单总结一下:红黑树的根是黑的,没有连续的红节点,每条路径都有相同数量的黑节点。

树中最短的路径:全黑的路径;树中最长的路径:一黑一红的路径 -> 所以最多是2倍。

满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点个数的两倍。

3.红黑树节点的实现

在节点的定义中,要将节点的默认颜色给成红色的,这样便于完成后续的操作。

enum Colour
{
    BLACK,
    RED
};

template<class K, class V>
class RBTreeNode
{
public:
    RBTreeNode(const pair<K, V>& kv, Colour colour = RED)
        :_left(nullptr)
        ,_right(nullptr)
        ,_parent(nullptr)
        ,_kv(kv)
        ,_colour(colour)
    {}
public:
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;
    pair<K, V> _kv;
    Colour _colour;
};

二、红黑树的操作

(一)红黑树的插入

红黑树的插入分为两个大步骤:1.按照二叉搜索树插入  2.做出对应的调节,使得其变为红黑树

1.按照二叉搜索树插入:这一步到现在对大家来说已经非常熟练了,所以直接展示这部分的代码

//按照搜索二叉树的规则进行插入
//空树插入
if(_root == nullptr)
{
    _root = new Node(kv, BLACK);//保证根节点是黑色的
    return true;
}
//非空树插入
//找到要插入的位置
Node* cur = _root;
Node* parent = nullptr;
while(cur)
{
    if(kv.first < cur->_kv.first)
    {
        parent = cur;
        cur = cur->_left;
    }
    else if(kv.first > cur->_kv.first)
    {
        parent = cur;
        cur = cur->_right;
    }
    else
        return false;
}
//新建节点
cur = new Node(kv);
//连接
if(kv.first < parent->_kv.first)
{
    parent->_left = cur;
    cur->_parent = parent;
}
else
{
    parent->_right = cur;
    cur->_parent = parent;
}

2.做出对应的调节,使得其变为红黑树:这一步又分成三种情况,下面逐一讲解

(1)第一种情况:空树的插入 ==> 插入节点做根,并且把他变黑

(2)第二种情况:cur为红,parent为黑 ==> 直接插入就可以

(3)第三种情况:cur为红,parent为红,这种情况可以推断出grandfather一定存在且为黑。

        遇到这种情况,关键看uncle的情况,根据uncle的情况,下面又分出了三种情况

①uncle存在且为红,这种情况所对应的操作:

将parent和uncle变为黑,grandfather变为红,然后继续向上调整grandfather

还要注意⚠️:这种情况g、p、u这几个节点在左边还是右边都是这么处理

742efe232e894fb4b097f5c4a557f852.png

②uncle不存在/uncle存在且为黑,这种情况所对应的操作:旋转 + 变换

    1)如果grandfather、parent、cur是一条直线

        a)parent为grandfather的左孩子,cur为parent的左孩子,则对grandfather进行右单旋 

        b)parent为grandfather的右孩子,cur为parent的右孩子,则对grandfather进行左单旋

        变色:parent、grandfather变色 -> parent变黑,grandfather变红

673106dcd51d49cab34f92351678ef97.jpeg

    2)如果grandfather、parent、cur是一条折线

        a)parent为grandfather的左孩子,cur为parent的右孩子 ==>

                对parent左单旋,在对grandfather右单旋

        b)parent为grandfather的右孩子,cur为parent的左孩子 ==>

                对parent右单旋,在对grandfather左单旋

        变色:cur、grandfather变色 -> cur变黑,grandfather变红

45d97953a79f47adbe957aa7b6f41951.jpeg

上图只展示了一次旋转,因为在进行了一次旋转后,就转换成了g、p、cur在一条直线的情况。

下面展示完整的插入代码

bool Insert(const pair<K, V>& kv)
{
    //1.按照搜索二叉树的规则进行插入
    if(_root == nullptr)
    {
        _root = new Node(kv, BLACK);//保证根节点是黑色的
        return true;
    }
    //找到要插入的位置
    Node* cur = _root;
    Node* parent = nullptr;
    while(cur)
    {
        if(kv.first < cur->_kv.first)
        {
            parent = cur;
            cur = cur->_left;
        }
        else if(kv.first > cur->_kv.first)
        {
            parent = cur;
            cur = cur->_right;
        }
        else
            return false;
    }
    cur = new Node(kv);
    //连接
    if(kv.first < parent->_kv.first)
    {
        parent->_left = cur;
        cur->_parent = parent;
    }
    else
    {
        parent->_right = cur;
        cur->_parent = parent;
    }
    
    //做出对应的调节,使得其变为红黑树
    while(parent && parent->_colour == RED)
    {
        //红黑树的条件关键看叔叔
        Node* grandfather = parent->_parent;
        //parent是grandfather的左孩子
        if(grandfather->_left == parent)
        {
            Node* uncle = grandfather->_right;//叔叔就是右孩子
            //遇到情况三的①:cur为红,parent为红,grandfather为黑,uncle存在且为红
            if(uncle && uncle->_colour == RED)
            {
                //将parent和uncle变为黑,grandfather变为红
                parent->_colour = uncle->_colour = BLACK;
                grandfather->_colour = RED;
                
                //继续往上处理
                cur = grandfather;
                parent = cur->_parent;
            }
            //uncle为黑或者不存在
            else
            {
                //情况三中折线的情况:对parent左单旋,在对grandfather右单旋
                //下面只完成了对parent左单旋,因为这样就将折线转换成了直线
                if(cur == parent->_right)
                {
                    RotateL(parent);
                    swap(parent, cur);
                }
                //完成对grandfather右单旋:
                //有可能直接是情况三中直线的情况
                //也有可能是由情况三中折线的情况 变换过来的
                RotateR(grandfather);
                grandfather->_colour = RED;
                parent->_colour = BLACK;
            }
        }
        //parent是grandfather的右孩子
        else
        {
            Node* uncle = grandfather->_left;//叔叔就是左孩子
            //uncle存在且为红
            if(uncle && uncle->_colour == RED)
            {
                parent->_colour = uncle->_colour = BLACK;
                grandfather->_colour = RED;
                
                //向上调整grandfather
                cur = grandfather;
                parent = cur->_parent;
            }
            //uncle为黑或者不存在
            else
            {
                //完成折线的parent右单旋
                if(cur == parent->_left)
                {
                    RotateR(parent);
                    swap(parent, cur);
                }
                //完成直线的grandfather左单旋
                RotateL(grandfather);
                grandfather->_colour = RED;
                parent->_colour = BLACK;
            }
        }
    }
    //强制将根改成黑色,保持性质
    _root->_colour = BLACK;
    
    return true;
}
//旋转处理
//1.左单旋
void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    Node* ppNode = parent->_parent;//指向parent节点的父亲
    
    //1.subR的左边放在parent的右边
    //!!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
    parent->_right = subRL;//连儿子
    if(subRL)//连父亲
        subRL->_parent = parent;
    
    //2.parent变成subR的左边
    //同样 !!因为是用三叉链表示的树,所以不仅需要连接儿子,还需要连接父亲!!
    subR->_left = parent;
    parent->_parent = subR;
    //找subR的父亲
    //(1)原来parent是这棵树的根,现在subR成了这棵树的根
    if(_root == parent)
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    //(2)parent为根的树只是整棵树的子树
    else
    {
        //连孩子
        //parent是一个左子树
        if(ppNode->_left == parent)
            ppNode->_left = subR;
        //parent是一个右子树
        else
            ppNode->_right = subR;
        
        //连父亲
        subR->_parent = ppNode;
    }
}

//2.右单旋
void RotateR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    Node* ppNode = parent->_parent;//指向parent节点的父亲
    
    //1.subL的右边放到parent的左边
    parent->_left = subLR;
    if(subLR)
        subLR->_parent = parent;
    
    //2.parent变成subL的右边
    subL->_right = parent;
    parent->_parent = subL;
    //找subL的父亲
    //(1)原来parent是这棵树的根,现在subL成了这棵树的根
    if(_root == parent)
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    //(2)parent为根的树只是整棵树的子树
    else
    {
        //连孩子
        //parent是一个左子树
        if(ppNode->_left == parent)
            ppNode->_left = subL;
        //parent是一个右子树
        else
            ppNode->_right = subL;
        //连父亲
        subL->_parent = ppNode;
    }
}

(二)红黑树的删除

删除在这里不详细介绍了,给大家推荐一篇优秀的博客,是关于红黑树删除操作的。

https://www.cnblogs.com/fornever/archive/2011/12/02/2270692.html

(三)红黑树的查改

因为红黑树本质上还是一个搜索二叉树,所以他们的查改都一样,这里就不详细介绍了。

(四)红黑树的验证

红黑树的验证分成两步:1.验证中序遍历有序  2.验证红黑树的五条性质

由于这里不是重点,所以我们不详细分析,直接上验证红黑树的五条性质的代码

//验证五条性质
bool _IsValidRBTree(Node* root, size_t k, const size_t blackCount)
{
    //走到null之后,判断k和black是否相等
    if (nullptr == root)
    {
        if (k != blackCount)
        {
            cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
            return false;
        }
        return true;
    }
    // 统计黑色节点的个数
    if (BLACK == root->_colour)
        k++;
    // 检测当前节点与其双亲是否都为红色
    Node* parent = root->_parent;
    if (parent && RED == parent->_colour && RED == root->_colour)
    {
        cout << "违反性质三:没有连在一起的红色节点" << endl;
        return false;
    }
    return _IsValidRBTree(root->_left, k, blackCount) &&
        _IsValidRBTree(root->_right, k, blackCount);
}

bool IsValidRBTree()
{
    Node* root = _root;
    // 空树也是红黑树
    if (nullptr == root)
        return true;
    // 检测根节点是否满足情况
    if (BLACK != root->_colour)
    {
        cout << "违反红黑树性质二:根节点必须为黑色" << endl;
        return false;
    }
    // 获取任意一条路径中黑色节点的个数
    size_t blackCount = 0;
    Node* cur = root;
    while (cur)
    {
        if (BLACK == cur->_colour)
            blackCount++;
        cur = cur->_left;
    }
    // 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
    size_t k = 0;
        return _IsValidRBTree(root, k, blackCount);
}

三、红黑树的性能

红黑树增删查改时间复杂度是:o(logN)

最短路径是:o(logN)     最长路径是:2*o(logN)

也就是说理论上而言,红黑树的效率比AVL树略差。但是现在呢,硬件的运算速度非常快,他们

之间己经基本没有差异了。因为常规数据集中logN足够小,与2*logN之间的差异不大。

为什么AVLTree和红黑树之间的性能基本差了2倍,但是我们认为基本上是一样的呢?

因为现在的硬件足够快:比如10亿个数查找,AVLTree最多查找30次;红黑树最多查找60次。30

次的查找和60次的查找对于现在的硬件基本是一样的。

为什么实际中红黑树得到了更广泛的应用,而不是AVL树?

第一点是因为插入刪除同样节点红黑树比AVL树旋转更少,AVLTree更严格的平衡其实是通过多

旋转达到的,更多次的旋转导致了效率的损耗。第二点是因为红黑树在实现上比AVL树更容易控

制。所以实际中红黑树得到了更广泛的应用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值