C++:红黑树

1. 红黑树的概念

红黑树是一棵二叉搜索树,它的每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个结点的颜色进行约束,**红黑树确保没有一条路径会比其他路径长出 2 倍,因而接近平衡。**也就是说,如果一棵树的最短路径是:h,那最长路径不会大于2h。这里所说的路径,指的是从根节点到nullptr节点的这段路程。

所以,可以说红黑树是一个近似二叉平衡搜索树的树。

2. 红黑树的规则

  1. 每个结点不是红色就是黑色。

  2. 根结点是黑色的。

  3. 如果一个结点是红色的,则它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点。

  4. 对于任意一个结点,从该结点到其所有 NULL 结点的简单路径上,均包含相同数量的黑色结点。

我们要思考一个问题:对于红黑树来说,最短路径和最长路径的特征是什么?

以这个树为例:

因为路径指的是从根节点到nullptr节点的这段路程,那么此图中最短路径就是最左侧的两个黑色节点所组成的路径,最长路径就是最右侧的两个路径。因此最短路径的特征是:全部由黑色节点组成的路径;最长路径的特征是:由红黑相间的节点组成的路径。

说明:《算法导论》等书籍上补充了一条每个叶子结点 (NIL) 都是黑色的规则。他这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把 NIL 叫做外部结点。就如下图所示:

NIL 是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了 NIL 结点,所以我们知道一下这个概念即可。

3. 红黑树的效率

假设 N 是红黑树树中结点数量,h 最短路径的长度,以下图为例:

因为对于一个满二叉树来说,二叉树高度为h,则总节点个数就是 2^h - 1 。因此对于红黑树来说,结点数量和最短路径长度h的关系就是: 2h − 1 <= N < 2^(2∗h) −1, 由此推出 h ≈ logN,也就是意味着红黑树增删查改最坏也就是走最长路径2∗logN,那么时间复杂度还是O(logN)。

红黑树的表达相对 AVL 树要抽象一些,AVL 树通过高度差直观的控制了平衡。红黑树通过 4 条规则的颜色约束,间接的实现了近似平衡,他们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为他对平衡的控制没那么严格。

4. 红黑树的模拟实现

4.1 红黑树的结构

这里使用的enum是C++ 中的枚举类型定义,用来表示红黑树节点的颜色(红色 / 黑色)。

enum是 C++ 的关键字,用于定义枚举类型 (把一组离散的、有限的取值,用符号化的名称表示);这里定义的枚举类型名为Colour(颜色);枚举类型的取值(枚举常量)是RED和BLACK,分别对应 "红色" 和 "黑色"------ 后续在红黑树的节点结构体中,可以用Colour _col;这样的成员变量,来存储节点的颜色。

剩下的部分就是和AVL树的结构一模一样了,都有左右子节点和父节点变量,并且都有key-value值。

4.2 红黑树的插入

4.2.1 插入的思路

插入一个值按二叉搜索树规则进行插入插入后我们只需要观察是否符合红黑树的 4 条规则。

如果是空树插入,新增结点是黑色结点。如果是非空树插入,新增结点必须红色结点,因为非空树插入,新增黑色结点就破坏了规则 4,规则 4 是很难维护的。

非空树插入后,新增结点必须红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束。

非空树插入后,新增结点必须红色结点,如果父亲结点是红色的,则违反规则 3。

进一步分析,c 是红色,p 为红,g 必为黑,这三个颜色都固定了,因为如果g是红色,那么在插入之前这棵就是错的。但是u的颜色不能确定,所以关键的变化看 u 的情况,需要根据 u 分为以下三种情况分别处理:1. u 不存在。 2. u 存在且为黑。 3. u 存在且为红。

说明:图中假设我们把新增结点标识为 c (cur),c 的父亲标识为 p (parent),p 的父亲标识为 g (grandfather),p 的兄弟标识为 u(uncle)。

4.2.2 变色

因为上面说到:新增节点都是先假设为红色,那有可能就会出现新增节点的父节点也是红色,这就影响了规则三,因此涉及到变色的概念,以下图为例:

单纯变色的场景出现时,一定是:c 为红,p 为红,g 为黑,u 存在且为红。

则将 p 和 u 变黑,g 变红。在把 g 当做新的 c,继续往上更新。

分析:因为 p 和 u 都是红色,g 是黑色,把 p 和 u 变黑,左边子树路径各增加一个黑色结点,g 再变红,相当于保持 g 所在子树的黑色结点的数量不变,同时解决了 c 和 p 连续红色结点的问题。

需要继续往上更新是因为,g 是红色,++如果 g 的父亲还是红色++ ,就回到了和刚刚重复的问题,需++要把g变成新的c,然后继续处理++ ;++如果 g 的父亲是黑色,则处理结束了++ ;++如果 g 就是整棵树的根,再把 g 变回黑色。++

情况 1 只变色,不旋转。所以无论 c 是 p 的左还是右,p 是 g 的左还是右,都是上面的变色处理方式。

跟 AVL 树类似,图 0 我们展示了一种具体情况,但是实际中需要这样处理的有很多种情况。

图 1 将以上类似的处理进行了抽象表达,看场景二的最左侧的图,d/e/f 代表每条路径拥有 hb 个黑色结点的子树,a/b 代表每条路径拥有 hb-1 个黑色结点的根为红的子树,hb>=0。

这里a/b之所以是hb-1,因为x的颜色是黑,如果包含a/b的父节点x,那a/b的黑色节点也是hb个。

图 2 / 图 3 / 图 4,分别展示了 hb==0 / hb==1 / hb==2 的具体情况组合分析,当 hb 等于 2 时,这里组合情况上百亿种,这些样例是帮助我们理解,不论情况多少种,多么复杂,处理方式一样的,变色再继续往上处理即可,所以我们只需要看抽象图即可。

展示四种情况只是告诉大家这个情况非常复杂,但是变色的逻辑还是一样的。接下来我们来实现代码:

这里主要需要注意最后一句代码,_root->_col = BLACK,因为单纯变色完之后有两种结果:1. 如果 g 的父亲是黑色,则处理结束了 2. 如果 g 就是整棵树的根,再把 g 变回黑色。所以这里不管三七二十一,都将根节点置为黑色,就能同时满足这两种情况。

4.2.3 变色+单旋

变色+单旋的场景出现时,一定是:c 为红,p 为红,g 为黑,u 不存在或者 u 存在且为黑。

u 不存在,则 c 一定是新增结点;u 存在且为黑,则 c 一定不是新增,c 之前是黑色的。

分析: p 必须变黑,才能解决连续红色结点的问题,u 不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转 + 变色。

如果u不存在,有以下两种情况:

如果 p 是 g 的左,c 是 p 的左,那么以 g 为旋转点进行右单旋,再把 p 变黑,g 变红即可。p 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 p 的父亲是黑色还是红色或者空都不违反规则。

如果 p 是 g 的右,c 是 p 的右,那么以 g 为旋转点进行左单旋,再把 p 变黑,g 变红即可。p 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 p 的父亲是黑色还是红色或者空都不违反规则。这里就和AVL树中的左单旋场景一样,不再赘述。

如果u存在且为黑,如下图所示:

在这里对于a/b两个子树来说,是因为有新插入的节点插入在了a/b子树上,新增节点是红色,a/b子树的根节点也是红色,这就引发了单纯变色,于是a/b子树的根节点变黑,所以a/b子树当中黑色节点就多了一个,于是黑色节点数量就从hb-1变成了hb。再让c节点变红,这时的场景是:c为红,p为红,u存在且为黑,就需要变色再加单旋。

接下来实现代码:

4.2.4 变色+双旋

c 为红,p 为红,g 为黑,u 不存在或者 u 存在且为黑,u 不存在,则 c 一定是新增结点,u 存在且为黑,则 c 一定不是新增,c 之前是黑色的,是在 c 的子树中插入,符合情况 1,变色将 c 从黑色变成红色,更新上来的。

分析:p 必须变黑,才能解决连续红色结点的问题,u 不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转 + 变色。

如果 p 是 g 的左,c 是 p 的右,那么先以 p 为旋转点进行左单旋,再以 g 为旋转点进行右单旋,再把 c 变黑,g 变红即可。c 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 c 的父亲是黑色还是红色或者空都不违反规则。

如果 p 是 g 的右,c 是 p 的左,那么先以 p 为旋转点进行右单旋,再以 g 为旋转点进行左单旋,再把 c 变黑,g 变红即可。c 变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为 c 的父亲是黑色还是红色或者空都不违反规则。

这里的逻辑和变色+单旋几乎一样,只是要进行两次单旋。

接下来实现代码:

5. 红黑树的平衡检测

和AVL树一样,插入完成之后我们也需要判断一下是否是一个合格的红黑树。

我们前面讲到AVL树的平衡检测的时候,是计算每个节点的左右子树的高度,再计算高度差,然后和该节点的平衡因子作比较。但是对于红黑树来说,如果沿着这个思路走,我们去写一个算法,找到红黑树的最短路径和最长路径,然后比较它们俩之间的大小有没有超过两倍。但除了最长最短路径的关系之外,还要考虑到红黑树当中有没有连续的两个红色节点。

因此我们检查红黑树是否平衡,还是去看每个节点是否满足红黑树的四个规则,只要能满足这四个规则,也就一定能做到最长路径不超过最短路径的两倍。

红黑树的四个规则:

  1. 每个结点不是红色就是黑色

  2. 根结点是黑色的

  3. 如果一个结点是红色的,则它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点。

  4. 对于任意一个结点,从该结点到其所有 NULL 结点的简单路径上,均包含相同数量的黑色结点

每个规则的方法:

  1. 规则 1 枚举颜色类型,天然实现保证了颜色不是黑色就是红色。

  2. 规则 2 直接检查根即可

  3. 规则 3 前序遍历检查,遇到红色结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了,因为红色结点的父亲一定是黑色结点,并且必然存在,如果不是那就证明这棵树有问题。

  4. 规则 4 前序遍历,遍历过程中用形参记录跟到当前结点的 blackNum (黑色结点数量),前序遍历遇到黑色结点就 ++blackNum,走到空就计算出了一条路径的黑色结点数量。再任意一条路径黑色结点数量作为参考值,依次比较即可。

在这段代码种,IsBalance函数检查了根节点是否为红色,并且调用Check函数,在Check函数种使用递归实现前序遍历,去检查是否有连续的红色节点出现。那么对于规则四,我们使用一个形参去记录黑色节点的数量。

只得到一个黑色节点的数量还没有用,我们还需要得到一个基准值,在这里我们先遍历最左侧路径中的黑色节点数量,用blackNumRef去存储作为基准值,然后在递归的过程中,只要走到了nullptr节点,就代表这个路径中的黑色节点数量已经获取完了,那就和基准值进行比较,如果获取到的黑色节点数量和基准值不一样,那就说明这棵树有问题。

最后展示一下全部代码:

cpp 复制代码
#pragma once

enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	// 这里更新控制平衡也要加入parent指针

	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;
	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
	{}
};

template<class K, class V>
class RBTree
{
    typedef RBTreeNode<K, V> Node;
public:
    bool Insert(const pair<K, V>& kv)
    {
        if (_root == nullptr)
        {
            _root = new Node(kv);
            _root->_col = BLACK;
            return true;
        }

        Node* cur = _root;
        Node* 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;
            }
        }

        cur = new Node(kv);
        cur->_col = RED;

        if (parent->_kv.first < kv.first)
        {
            parent->_right = cur;
        }
        else
        {
            parent->_left = cur;
        }

        cur->_parent = parent;

        while (parent&& parent->_col = RED)
        {
            Node* grandfather = parent->_parent;
            if (grandfather->_left == parent)
            {
                Node* uncle = grandfather->_right;
                //u存在且为红,单纯变色即可
                if (uncle->_col == RED)
                {
                    uncle->_col = BLACK;
                    parent->_col = BLACK;
                    grandfather->_col = RED;

                    //继续向上处理
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else  //u不存在,或者u存在且为黑, 单旋+变色
                {
                    //如果是这种结构:
                    //        g
                    //     p     u
                    //   c
                    //      右单旋
                    if (cur == parent->_left)
                    {
                        RotateR(grandfather);
                        parent->_col = BLACK;
                        grandfather->_col = RED;              
                    }

                    //如果是这种结构:
                    //        g
                    //     p      u
                    //        c
                    //      左右双旋
                    else
                    {
                        if (cur == parent->right)
                        {
                            RotateL(parent);
                            RotateR(grandfather);

                            cur->_col = BLACK;
                            grandfather->_col = RED;
                        }
                    }
                    break;
                }
            }
            else  //这种情况是:grandfather->_right == parent
            {
                Node* uncle = grandfather->_left;
                //u存在且为红,单纯变色即可
                if (uncle->_col == RED)
                {
                    uncle->_col = BLACK;
                    parent->_col = BLACK;
                    grandfather->_col = RED;

                    //继续向上处理
                    cur = grandfather;
                    parent = cur->_parent;
                }
                else  //u不存在,或者u存在且为黑, 单旋+变色
                {
                    //如果是这种结构:
                    //        g
                    //     u     p
                    //              c
                    //      左单旋
                    if (cur == parent->_right)
                    {
                        RotateL(grandfather);
                        parent->_col = BLACK;
                        grandfather->_col = RED;
                    }

                    //如果是这种结构
                    //          g
                    //      u        p
                    //            c
                    //        右左双旋
                    else
                    {
                        if (cur == parent->_left)
                        {
                            RotateR(parent);
                            RotateL(grandfather);

                            cur->_col = BLACK;
                            grandfather->_col = RED;
                        }
                    }
                    break;
                }
            }
        }
        _root->_col = BLACK;  //将根节点置为黑色
        return true;
    }

    bool IsBalance()
    {
        if (_root == nullptr)
            return true;
        if (_root == RED)
            return false;

        Node* leftMost = _root;
        int blackNumRef = 0;
        while (leftMost)
        {
            if (leftMost->_col == BLACK)
            {
                ++blackNumRef;
            }
            leftMost = leftMost->_left;
        }

        return Check(_root,0 , blackNumRef);
    }
 private:
     bool Check(Node* cur , int blackNum, const int blackNumRef)
     {
         if (cur == nullptr)
         {
             if (blackNum != blackNumRef)
             {
                 cout << "黑色节点数量不同" << endl;
                 return false;
             }
             return true;

         }
         if (cur->_col == RED && cur->_parent && cur->_parent->_col == RED)
         {
             cout << cur->_kv.first << " -> " << "连续的红色节点" << endl;
             return false;
         }
         if (cur->_col == BLACK)
         {
             ++blackNum;
         }
         return Check(cur->_left , blackNum , blackNumRef)
             && Check(cur->_right , blackNum , blackNumRef);
     }



        void RotateR(Node* parent)
        {
            Node* subL = parent->_left;
            Node* subLR = subL->_right;

            parent->_left = subLR; //改变subL、subLR、parent之间的位置关系
            if (subLR != nullptr)
            {
                subLR->_parent = parent;
            }

            //改变三个节点的父子关系
            //先记录下原来的旋转点的父节点
            Node* parentParent = parent->_parent;

            subL->_right = parent;
            parent->_parent = subL;

            //下面改变subL的父节点
            if (parent == _root)  //旋转点是根节点
            {
                _root = subL;
                subL->_parent = nullptr;
            }
            else                  //旋转点是一个局部子树
            {
                if (parentParent->_left == parent)
                {
                    parentParent->_left = subL;
                }
                else
                {
                    parentParent->_right = subL;
                }
                subL->_parent = parentParent;
            }
        }

        void RotateL(Node* parent)
        {
            Node* subR = parent->_right;
            Node* subRL = subR->_left;

            //先改变位置关系
            parent->_right = subRL;
            subR->_left = parent;

            //改变父子关系
            Node* parentParent = parent->_parent;

            parent->_parent = subR;
            if (subRL != nullptr)
            {
                subRL->_parent = parent;
            }

            if (parent == _root)
            {
                _root = subR;
                subR->_parent = nullptr;
            }
            else
            {
                if (parentParent->_left == parent)
                {
                    parentParent->_left = subR;
                }
                else
                {
                    parentParent->_right = subR;
                }
                subR->_parent = parentParent;
            }
        }
private:
    Node* _root = nullptr;
};

本文到此结束,感谢各位读者的阅读,如果有讲解的不到位或者错误的地方,欢迎大家批评和指正。

相关推荐
冷雨夜中漫步7 小时前
Python快速入门(6)——for/if/while语句
开发语言·经验分享·笔记·python
半桔7 小时前
【IO多路转接】高并发服务器实战:Reactor 框架与 Epoll 机制的封装与设计逻辑
linux·运维·服务器·c++·io
HABuo8 小时前
【linux文件系统】磁盘结构&文件系统详谈
linux·运维·服务器·c语言·c++·ubuntu·centos
我在人间贩卖青春8 小时前
C++之多重继承
c++·多重继承
m0_736919109 小时前
C++代码风格检查工具
开发语言·c++·算法
2501_944934739 小时前
高职大数据技术专业,CDA和Python认证优先考哪个?
大数据·开发语言·python
黎雁·泠崖9 小时前
【魔法森林冒险】5/14 Allen类(三):任务进度与状态管理
java·开发语言
2301_7634724610 小时前
C++20概念(Concepts)入门指南
开发语言·c++·算法
阿猿收手吧!11 小时前
【C++】std::promise原理与实战解析
c++
TechWJ11 小时前
PyPTO编程范式深度解读:让NPU开发像写Python一样简单
开发语言·python·cann·pypto