【数据结构】红黑树

🐼为啥要有红黑树

由于AVL树旋转操作的及其频繁,代价很大,对平衡的要求很高。这里又有了一个树,能够在平衡要求不高的前提下,依旧保证查找效率很高


🐼红黑树的性质

红黑树是⼀棵⼆叉搜索树,顾名思义,它的节点颜色不是红色就是黑色。 通过对任何⼀条从根到叶子的路径上各个结点的颜⾊进⾏约束,红黑树确保没有⼀条路径会比其他路径⻓出2倍,因而是接近平衡的。

红黑树的以下几个性质,是必须严格遵守的,否则就不能被称为红黑树:

性质1:每个节点不是红色就是黑色

性质2:根节点必须是黑色的

这里的根节点不包括子树的根节点,而是整棵树唯一的_root节点

所以红黑树的左右子树不是红黑树(区分:AVL树的左右子树也是AVL树)

性质3:可以存在两个或多个连续的黑色节点,但是不能存在连续的红色节点

性质4:每条路径上的黑色节点数量必须相同
性质2、3和性质4就决定了红黑树的最长路径不超过最短路径的二倍,为什么,如图所示:

图中的红黑树已省略其他节点,只保留最长路径和最短路径

可以看到此时路径中的黑色节点数量相同,因此无法在最长路径中再添加一个黑色节点;又因为不能存在连续的红色节点,而最长路径中的最后一个节点为红色,因此也无法在最长路径中添加红色节点。

通过观察可以发现,在红黑树中全黑的路径必然是最短路径,而一黑一红交替的路径是最长路径

性质5:红黑树的空节点(NIL节点)默认为黑色

正是由于红黑树的最长路径不小于最短路径的2倍,才让红黑树有了一种相对平衡,代价小,但是有近似平衡的效果!


🐼如何实现一颗红黑树

红黑树依旧是一颗二叉搜索树,保持kv结构,唯一不同的是自已的每一个节点有了颜色

节点定义:

cpp 复制代码
enum Color
{
    RED,
    BLACK
};
template <typename K, typename V>
struct RBNode
{
    RBNode(const std::pair<K, V> &kv) : left(nullptr), right(nullptr), parent(nullptr), kv(kv) {}
    RBNode<K, V> *left;
    RBNode<K, V> *right;
    RBNode<K, V> *parent;

    std::pair<K, V> kv;
    Color col;
};

既然是二叉搜索树,那么在插入节点时,同样和二叉搜索树插入节点类似,只不过如果是根节点,需要将根节点颜色置为黑色,并且new出来的新节点保证是红色的。

💮可是为什么节点的初始颜色是红色?不能是黑色吗?

假设我们把节点的初始颜色设置为黑色,那么在我们插入一个新节点的时候会发生什么事呢?

如图,如果是黑色,就打破了红黑树的规则,该路径的黑色节点数目发生了变化,破坏了性质4,当红黑树的性质被破坏后,我们需要对其进行旋转或变色等操作使其重新成为一棵红黑树,那么再去调整的代价可太大了!!

但是如果新插入的节点是红色的,大体就分为两种情况

插入新节点,此时新节点的父节点为黑色

因为此时插入一个新节点没有破坏任何一个性质,所以不需要进行调整

插入新节点,此时新节点的父节点为红色

插入新节点后,出现了连续的红色节点,性质3被破坏,需要进行调整。

可以看出,如果我们将节点的初始颜色设置为红色,在某些情况下是不需要进行调整的

而当我们检测到插入的新节点的父节点是红色时,才需要进行调整

因此,基于这种情况,红黑树初始的每个节点为红色的。

下面我们先写出不基于调整的插入代码:

cpp 复制代码
    bool insert(const std::pair<K, V> kv)
    {
        if (nullptr == _root)
        {
            _root = new Node(kv);
            _size++;
            _root->col = BLACK; // 根节点为黑色
            _root->parent = nullptr;
            return true;
        }
        // 根节点不为空
        Node *cur = _root, *parent = nullptr;

        // 找到要插入的位置
        while (cur)
        {
            if (cur->kv.first < kv.first)
            {
                parent = cur;
                cur = cur->right;
            }
            else if (cur->kv.first > kv.first)
            {
                parent = cur;
                cur = cur->left;
            }
            else
            {
                return false; // 不能重复插入
            }
        }

        Node *newnode = new Node(kv);
        newnode->col = RED;       // 插入的都为红色,以便调整
        newnode->parent = parent; // 不要忘记置父节点
        if (newnode->kv.first < parent->kv.first)
        {
            parent->left = newnode;
        }
        else
        {
            parent->right = newnode;
        }

        // 调整逻辑
         
        _size++;
        _root->col = BLACK; // 无脑将根节点置为黑色
        return true;
    }

当插入一个新的红色节点,由于父节点也是红色的,我们需要进行调整。

调整大体总共分为三种情况:

插入新节点,父节点为红,叔叔节点存在且也为红

如图:

这种情况处理起来最好办,我们只需要将uncle和parent变为黑色,grandpa变成红色,由于grandpa还是红色,需要继续向上处理,因为保证不了grandpa->parent是黑色的

插入新节点,父节点为红,叔叔节点不存在或存在且为黑,父节点和祖父节点的相对位置与新节点与父节点的相对位置相同

cpp 复制代码
      g
   p     u
 c

或者
      g
  p        
c          

这里以叔叔节点存在且为黑来演示:

在调整的时候,我们需要时刻遵循每条路径的黑色节点数量相同的原则,因此a、b、c中的黑色节点必定比d、e中的黑色节点数目多1个(因为叔叔节点为黑色,已经为d、e中的路径提供了一个黑色节点)

这种情况我们可以分析一下,如果叔叔节点不存在,则当前节点⼀定是新增结点 ,因为如果不是新怎的,那么就是更新上来的,cur这条路径一定有黑色节点,这条路径就比叔叔节点的黑色节点多;u存在且为黑,则c⼀定不是新增 ,c之前是黑色的,是在c的子树中插⼊,符合情况1,变色将c从黑⾊变成红⾊,更新上来的

不过处理方式都是一样的,父节点必须变黑,才能解决,连续红色结点的问题,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊

当父节点在祖父节点的左边且新节点在父节点的左边时,先进行右单旋再变色

但是如果当父节点在祖父节点的右边且新节点在父节点的右边时,则先进行左单旋再变色

当叔叔节点不存在时,处理方法和叔叔节点存在且为黑的相同。

为什么不分析叔叔存在且为红?情况一已经分析了。

插入新节点,父节点为红,叔叔节点不存在或存在且为黑,父节点和祖父节点的相对位置与新节点与父节点的相对位置相反

祖父节点-父节点-新节点的连线之间会有一个明显的折返角度,也就是说当父节点在祖父节点的左边时,新节点在父节点的右边;而当父节点在祖父节点的右边时,新节点在父节点的左边。

cpp 复制代码
      g
   p     u
     c

或者
      g
  u        p
         c

以parent在grandpa的左侧,cur在parent的右侧为例,如图

这种情况的本质是需要将cur变为黑色,将cur作根即可:

上面的情况,当父节点在祖父节点的左边且新节点在父节点的右边时,先进行左单旋再进行右单旋,最后变色

如果当父节点在祖父节点的右边且新节点在父节点的左边时,则先进行右单旋再进行左单旋,最后变色

当叔叔节点不存在时,处理方法和叔叔节点存在且为黑的相同。

完整的插入代码:

cpp 复制代码
    bool insert(const std::pair<K, V> kv)
    {
        if (nullptr == _root)
        {
            _root = new Node(kv);
            _size++;
            _root->col = BLACK; // 根节点为黑色
            _root->parent = nullptr;
            return true;
        }
        // 根节点不为空
        Node *cur = _root, *parent = nullptr;

        // 找到要插入的位置
        while (cur)
        {
            if (cur->kv.first < kv.first)
            {
                parent = cur;
                cur = cur->right;
            }
            else if (cur->kv.first > kv.first)
            {
                parent = cur;
                cur = cur->left;
            }
            else
            {
                return false; // 不能重复插入
            }
        }

        Node *newnode = new Node(kv);
        newnode->col = RED;       // 插入的都为红色,以便调整
        newnode->parent = parent; // 不要忘记置父节点
        if (newnode->kv.first < parent->kv.first)
        {
            parent->left = newnode;
        }
        else
        {
            parent->right = newnode;
        }

        cur = newnode; // 让cur从newnode开始更新!别忘记了,不然cur有可能为空,一次定位到这里的bug,哈哈哈,也是牛逼了~
        // 调整逻辑
        while (parent && parent->col == RED) // 如果是黑色无需调整了。并且保证了parent 一定不能是根节点,因为根节点是黑色,这样grandpa才有意义
        {
            Node *grandpa = parent->parent;

            if (grandpa->left == parent) // 当parent在grandpa左边
            {
                Node *uncle = grandpa->right;
                if (uncle && RED == uncle->col) // 叔叔节点存在且为红
                {
                    parent->col = uncle->col = BLACK;
                    grandpa->col = RED;
                    // 继续向上更新
                    cur = grandpa;
                    parent = cur->parent;
                }
                else
                {
                    // 叔叔节点不存在或者叔叔节点存在且为黑
                    if (parent->left == cur) // parent在grandpa与cur在parent的位置同侧
                    {
                        RotateR(grandpa);
                        grandpa->col = RED;
                        parent->col = BLACK;
                    }
                    else // 有折角,双旋+变色
                    {
                        RotateL(parent);
                        RotateR(grandpa);
                        grandpa->col = RED;
                        cur->col = BLACK;
                    }
                }
            }
            else
            {
                // 右侧同理
                Node *uncle = grandpa->left;
                if (uncle && RED == uncle->col) // 叔叔节点存在且为红
                {
                    parent->col = uncle->col = BLACK;
                    grandpa->col = RED;
                    // 继续向上更新
                    cur = grandpa;
                    parent = cur->parent;
                }
                else
                {
                    // 叔叔节点不存在或者叔叔节点存在且为黑
                    if (parent->right == cur) // cur在parent的同侧
                    {
                        RotateL(grandpa);
                        grandpa->col = RED;
                        parent->col = BLACK;
                    }
                    else // 有折角,双旋+变色
                    {
                        RotateR(parent);
                        RotateL(grandpa);
                        grandpa->col = RED;
                        cur->col = BLACK;
                    }
                }
            }
        }
        _size++;
        _root->col = BLACK; // 无脑将根节点置为黑色
        return true;
    }

💮既然红黑树是一颗二叉搜索树,但又要满足二叉搜索性,我们来验证;

我们需要验证:不能出现连续的红色节点,最长路径不能大于最短路径的2倍,根节点必须为黑色,保证左右子树都必须满足这个性质,递归来处理:

cpp 复制代码
  bool IsRBTree()
    {
        // 随机遍历一条路径
        int blackCount = 0;
        Node *cur = _root;
        while (cur)
        {
            if (cur->col == BLACK)
                blackCount++;
            cur = cur->left;
        }
        return _IsRBTree(_root, blackCount, 0);
    }

bool _IsRBTree(Node *root, int totalBlackCount, int curCount)
    {
        if (root == nullptr)
        {
            return curCount == totalBlackCount;
        }

        if (root->col == RED && root->parent && root->parent->col == RED)
        {
            cout << "出现连续红色节点:" << root->kv.first << " " << root->parent->kv.first << endl;
            return false;
        }

        if (root->col == BLACK)
        {
            curCount += 1;
        }

        return _IsRBTree(root->left, totalBlackCount, curCount) && _IsRBTree(root->right, totalBlackCount, curCount);
    }

🐼AVL树pk红黑树

最后,我们来验证一下AVL树和红黑树的性能差异,如果仅仅是插入,其实很难看出来他们的差别:

比如随机插入10000000个数据:

即使插入有序的数据,红黑树也比AVL树高一点而已,这对查找效率毫无影响

AVL树更擅长于查找,因为树矮,差的速率快,如果频繁查找推荐AVL树,但是对于频繁插入或删除操作推荐红黑树。红黑树最大的优势就是删除快的多,红黑树删除最多3次旋转,AVL可能O(log n)次旋转

相关推荐
北顾笙9802 小时前
测开准备-day01数据结构力扣
数据结构
博界IT精灵3 小时前
栈在表达式求值中的应用(暂时看到视频3.3.2_1的25min19秒)
数据结构
北顾笙9804 小时前
测开准备-day03数据结构力扣
数据结构
仰泳的熊猫4 小时前
题目2571:蓝桥杯2020年第十一届省赛真题-回文日期
数据结构·c++·算法·蓝桥杯
尽兴-4 小时前
Redis7 底层数据结构解析
数据结构·数据库·缓存·redis7
逆境不可逃5 小时前
LeetCode 热题 100 之 33. 搜索旋转排序数组 153. 寻找旋转排序数组中的最小值 4. 寻找两个正序数组的中位数
java·开发语言·数据结构·算法·leetcode·职场和发展
leaves falling6 小时前
二分查找:迭代与递归实现全解析
数据结构·算法·leetcode
杰克尼6 小时前
知识点总结--01
数据结构·算法
咱就是说不配啊7 小时前
3.20打卡day34
数据结构·c++·算法