红黑树的插入(C++实现)

news2024/11/24 6:51:50

1. 红黑树

1.1 概念

红黑树是一种二叉搜索树,它是AVL树的优化版本。红黑树是每个节点都带有颜色属性的二叉搜索树,颜色为红色黑色

之所以选择“红色”是因为这是作者在帕罗奥多研究中心公司Xerox PARC工作时用彩色雷射列印机可以产生的最好看的颜色。另一种说法来自Guibas,是因为红色和黑色的笔是他们当时可用来绘制树的颜色。

1.2 性质

在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  1. 节点是红色或黑色;
  2. 根是黑色;
  3. 所有叶子都是黑色(叶子是NIL节点);
  4. 每个红色节点必须有两个黑色的子节点;
    • (或者说从每个叶子到根的所有路径上不能有两个连续的红色节点。)
    • (或者说不存在两个相邻的红色节点,相邻指两个节点是父子关系。)
    • (或者说红色节点的父节点和子节点均是黑色的。)
  5. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

下面是一个红黑树的图例:

img

关于图示中的NIL

图中使用NIL表示空叶子,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略。有这样的结论:所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。

关键特性

由于有上面的性质(主要是性质4)的限制,使得红黑树从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。

由性质4可以知道,最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

对于这个关键特性,可以用一个极端情况理解:红黑树的一个子树是另一个子树的长度的两倍。那么就查找而言,最坏的情况下红黑树也就比AVL树多查找了一次,最坏查找次数是 2 l o g 2 N 2log_2N 2log2N,得益于CPU算力的强大,这多出来的一倍对整体效率并没有影响(大O计数法也是这么做的)。红黑树放宽了左右高度限制,所以它的旋转次数变少了,而这样做能减少插入时多次旋转造成的性能损失,而这就是红黑树优于AVL树的原因。

2. 实现红黑树

2.1 定义红黑树结点类

和实现AVL树结点类类似,依然使用pair键值对作为结点值的部分。必要时,红黑树依然要进行旋转,所以和AVL一样,也要定义一个parent变量,用于存放结点的父结点的地址。

除此之外,红黑树新增了一个_col变量,用于表示结点的颜色。在此,使用枚举常量保存颜色。

enum Color // 使用枚举
{
    RED,
    BLACK
};
// 红黑树结点类
template<class K, class V>
struct RBTreeNode
{
    RBTreeNode<K, V>* _left;
    RBTreeNode<K, V>* _right;
    RBTreeNode<K, V>* _parent;

    Color _col;                         // 颜色
    pair<K, V> _kv;

    RBTreeNode(const pair<K, V>& kv)
            : _left(nullptr)
            , _right(nullptr)
            , _parent(nullptr)
            , _kv(kv)
            , _col(RED)                 // 默认插入结点为红色
    {}
};

默认插入的颜色为什么是红色?

如果是红色,可能会违反红黑树的性质4,但如果是黑色,一定会违反红黑树的性质5,这是性质4和性质5之间的抗衡。

image-20221119215145489

就如示例中的这棵红黑树,如果在11的右孩子处新增红色结点,那么刚刚好不会破坏红黑树的性质,但如果在27的右孩子处新增黑色结点,会违反红黑树的规则5:从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点,这样会有一条路径的黑色结点比其他结点多一个。

对于插入结点颜色的设置,因为两种颜色都(可能)会违反规则,破坏红黑树的性质,所以选择影响较小的方式:默认新插入结点为红色。不论哪种方式,后续都有其对应的解决办法,即变色和旋转。

2.2 插入

由于红黑树是一种二叉搜索树,新插入结点必须在一个适合自己大小的位置插入,所以在真正插入结点之前还是二叉搜索树查找位置的逻辑。

红黑树新增了颜色的限制,去除了AVL树高度限制,所以插入以后只要根据颜色的情况采取对应操作。

插入步骤:

  1. 按二叉搜索树的插入步骤找到插入的位置;
  2. 插入新结点;
  3. 如果新插入的父结点是红色,要对红黑树调整。

为了理解和代码实现的方便,在下面的示例中用cur表示当前结点,用parent表示cur的父结点,用uncle表示cur的叔叔(即parent的另一个兄弟),用grandparent表示cur的祖父结点(即parent的父结点)。

插入结点之后,不一定要对树进行调整,只有当parent为红色时才需要。因为当当parent为红色,说明parent不是根结点(注意根结点是黑色的),所以grandparent一定存在,故parent为黑色时不影响红黑树的性质(以图理解)。

情况1:uncle存在且为红

抽象图能表示一类能用相同方式解决的所有情况:

image-20221119230018886

而叔叔存在且为红最简单的情况就是abcde都是空树,一个最简单的例子:

image-20221119223918132

然而,调整到此还未结束,因为我们要用一般情况看待调整的对象,即变色和旋转的对象都是整棵树的其中一棵子树。所以对grandparent而言,还需要再讨论它是否是根结点:

  • 如果grandparent是根结点,将parent置黑,相当于每条路径的黑结点都多了一个;
  • 如果grandparent不是根结点,说明它就是一棵子树,那么对于整棵树而言,这棵调整以后的子树就是新子树,要将新插入结点cur更新为grandparent插入原树。

上面的操作可能不止一次地被执行,因为此次插入新结点以后,grandparent的父结点也可能是红色的。

不论cur是parent的左右孩子,不论abcde是否为空,处理方式都是将parent和uncle置黑,将grandparent置红。

情况2:uncle存在且为黑

这种情况只可能是情况1变色调整过程中出现的,因为在变色之前叔叔如果存在的话不可能为黑,否则这一条路径下的黑色结点的个数就比其他路径多1了。

image-20221120124551147

而且cur一定是上次调整以后更新的grandparent。

当叔叔存在且为黑时,不仅单纯需要变色,而且需要旋转。根据cur、parent和grandparent之间的结构,可以分为单旋和双旋。

  • c、p、g呈直线,单旋+变色;
  • c、p、g呈折线,双旋+变色。

c、p、g呈直线

cur、parent和grandparent呈直线时,根据uncle是否存在,还可以分为两种情况。

uncle存在

下面的例子能更好地理解叔叔结点存在且为黑的原因:

image-20221120131926142

此例中第二棵树中的cur就是上一棵子树中插入新结点、调整颜色以后的grandparent。

从结果来看(就像分析AVL树的旋转过程一样),对于整棵树而言,插入新结点以后,它的左子树红色结点的个数大于右子树红色结点的个数,所以可以简单地理解为向右旋转后会把左边多出来的红色结点甩到右边。

事实上旋转并不能达到这种效果,但是旋转以后改变了cur、parent和grandparent的相对位置,当旋转完毕以后执行“parent和uncle变黑,grandparent变红”后,变能达到这种效果。

再来重新审视这个步骤:parent和uncle变黑,grandparent变红:

  • 旋转之前:

    image-20221120152057394

  • 旋转之后:

    image-20221120152952198

旋转以后再执行变色操作,可以将原来左边的红色结点“甩”到右边。

uncle不存在

当cur、parent和grandparent呈折线,而uncle不存在,则说明parent一定是红色的,这也说明cur一定是新插入的结点而不是颜色调整以后的上一次处理的grandparent。uncle为空,说明以grandparent为根结点的这棵子树只有一个子树,假如cur是上次处理后的grandparent,那么上次处理的子树中一定会多一个黑色结点(parent和uncle变黑),这样以grandparent为根结点的左右子树的黑色节点的个数就不相同了。

image-20221120163718577

c、p、g呈折线

当cur、parent和grandparent呈折线时,根据uncle结点存在与否,也可以分为两种情况。

uncle存在

最简单的例子:

image-20221120145015742

旋转的目的是一样的,将左边多余的红色结点“甩”到右边。由于最后这棵子树的根结点的颜色被更新为黑色,所以不用再往上更新了。

uncle不存在

当祖孙三代结点呈折线时,uncle不存在也说明cur是新插入结点,原因同上。

image-20221120164928941

注意

上面的每种情况都存在对称的情况,所以在代码中会有多个if…else语句区分左右子树,操作都是镜像的。

代码

// 插入函数
    bool Insert(const pair<K, V>& kv)
    {
        if (_root == nullptr)                   // 空树
        {
            _root = new Node(kv);
            return true;
        }

        Node *cur = _root;
        Node *parent = nullptr;

        while (cur)                             // 迭代,找到插入位置
        {
            if (kv.first < cur->_kv.first)      // 插入的值比key值小
            {
                parent = cur;
                cur = cur->_left;               // 往左走
            }
            else if (kv.first > cur->_kv.first) // 插入的值比key值大
            {
                parent = cur;
                cur = cur->_right;              // 往右走
            }
            else                                // 找不到
            {
                return false;
            }
        }
                                                // 跳出循环,说明找到插入的位置了
        cur = new Node(kv);                     // 将cur更新为新插入结点
        if (cur->_kv.first < parent->_kv.first) // 新结点值比叶子(父)结点小
        {
            parent->_left = cur;                // 作为父结点的左孩子插入
            cur->_parent = parent;
        }
        else
        {
            parent->_right = cur;
            cur->_parent = parent;
        }
                                                // 插入成功

                                                // 检查并调整颜色
        while (parent && parent->_col == RED)   // 父结点非空且为红,说明它是子树的根结点
        {
            Node *grandfather = parent->_parent;// 祖父结点
                                                // parent的位置分两种情况
            if (parent == grandfather->_left)   // (1). 父结点是祖父节点的左孩子
            {
                Node *uncle = grandfather->_right; // 叔叔就是祖父节点的另一个孩子
                if (uncle != nullptr && uncle->_col == RED) // 情况1:叔叔存在且为红
                {
                    parent->_col = BLACK;       // 父结点变黑
                    uncle->_col = BLACK;        // 叔叔结点变黑
                    grandfather->_col = RED;    // 祖父结点变红

                    cur = parent;
                    parent = parent->_parent;   // 继续向上处理
                }
                else                            // 跳出了上面的判断,有两种有效组合:叔叔为空,叔叔为黑
                {
                                                // 情况2:叔叔存在且为黑,右单旋+变色
                                                //     g    右旋       p
                                                //   p   u  -->   cur    g
                                                // cur                     u
                    if (cur == parent->_left)   // cur是parent的左子树
                    {
                        RotateR(grandfather);  // 以祖父结点为轴心右旋

                        parent->_col = BLACK;   // 父节点变黑
                        grandfather->_col = RED;// 祖父结点变黑
                    }
                    else                        // cur是parent的右子树
                    {
                                                // 情况3:
                                                //    g   左右旋     c
                                                //  p   u  -->    p   g
                                                //    c                 u
                        RotateR(grandfather); // 以祖父结点为轴心右旋

                        parent->_col = RED;     // 父节点变黑
                        grandfather->_col = BLACK; // 祖父结点变黑
                    }
                    break;                      // 旋转后子树根节点变黑,停止向上调整
                }
            }
            else                                // (2). 父结点是祖父节点的右孩子,步骤相同
            {
                Node *uncle = grandfather->_left;
                if (uncle && uncle->_col == RED)
                {
                    uncle->_col = parent->_col = BLACK;
                    grandfather->_col = RED;

                    cur = grandfather;
                    parent = cur->_parent;
                }
                else
                {
                    if (cur == parent->_left)
                    {
                        RotateRL(grandfather);

                        cur->_col = BLACK;
                        grandfather->_col = RED;
                    }
                    else
                    {
                        RotateL(grandfather);

                        grandfather->_col = RED;
                        parent->_col = BLACK;
                    }
                    break;
                }
            }
        }
        _root->_col = BLACK;                    // 不论根节点何种颜色,统一处理为黑色
    }

// 右单旋函数
void RotateR(Node *parent)
{
    Node *subL = parent->_left;
    Node *subLR = subL->_right;
    Node *pParent = parent->_parent;        // 保存父结点的父结点

    parent->_left = subLR;                  // 重建subLR和parent联系
    if (subLR != nullptr)
    {
        subLR->_parent = parent;
    }

    subL->_right = parent;                  // 重建subL和parent联系
    parent->_parent = subL;


    if (parent == _root)                    // 父结点为根结点,旋转后的subL作为根结点,无父结点
    {
        _root = subL;
        subL->_parent = nullptr;
    }
    else
    {
        if (pParent->_left == parent)
        {
            pParent->_left = subL;
        }
        else
        {
            pParent->_right = subL;
        }

        subL->_parent = pParent;
    }
}
// 左单旋函数
void RotateL(Node *parent)
{
    Node *subR = parent->_right;
    Node *subRL = subR->_left;
    Node *pParent = parent->_parent;        // 保存父结点的父结点

    parent->_right = subRL;                 // 重建subRL和parent联系
    if (subRL != nullptr)
    {
        subRL->_parent = parent;
    }

    subR->_left = parent;                   // 重建subR和parent联系
    parent->_parent = subR;


    if (parent == _root)                    // 父结点为根结点,旋转后的subR作为根结点,无父结点
    {
        _root = subR;
        subR->_parent = nullptr;
    }
    else
    {
        if (pParent->_left == parent)
        {
            pParent->_left = subR;
        }
        else
        {
            pParent->_right = subR;
        }

        subR->_parent = pParent;
    }
}
// 左右双旋函数
void RotateLR(Node *parent)
{
    Node *subL = parent->_left;
    Node *subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(subL);
    RotateR(parent);
}
// 右左双旋函数
void RotateRL(Node *parent)
{
    Node *subR = parent->_right;
    Node *subRL = subR->_left;
    int bf = subRL->_bf;

    RotateR(subR);
    RotateL(parent);
}

2.3 红黑树的验证

红黑树是一种二叉搜索树,所以最基本地,用中序遍历查看结点值是否有序。其次要用红黑树的规则验证。

//中序遍历
void Inorder()
{
	_Inorder(_root);
    cout << endl;
}
//中序遍历子函数
void _Inorder(Node* root)
{
	if (root == nullptr)
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

验证左右子树黑结点的个数是否相等。

由于红黑树的每条路径中黑色结点的个数都相等,所以可以任意取一条路径中的黑色结点个数作为基准值。怎么取才最方便呢?对于一棵树,它的最左路径和最右路径是最好走的,所以在这里以最左路径的黑色结点个数为基准值。

只需要验证是否是红黑树,而不用找到具体哪一条路径有问题,所以只要某条路径和基准值是否相等即可判断是否是红黑树。

采用递归方式:

//判断是否为红黑树
bool ISRBTree()
{
	if (_root == nullptr)
	{
		return true;
	}
    
	if (_root->_col == RED)
	{
		cout << "error:根结点为红色" << endl;
		return false;
	}
	
	// 以最左路径的黑色结点数做为的参考值
	Node* cur = _root;
	int BlackCount = 0;
	while (cur)
	{
		if (cur->_col == BLACK)
			BlackCount++;
		cur = cur->_left;
	}

	int count = 0;
	return _ISRBTree(_root, count, BlackCount);
}
// ISRBTree的子函数
bool _ISRBTree(Node* root, int count, int BlackCount)
{
	if (root == nullptr) // 该路径走到空
	{
		if (count != BlackCount) // 黑色结点数量和基准值不相等
		{
			cout << "error:黑色结点的数目不相等" << endl;
			return false;
		}
		return true;
	}

	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << "error:存在连续的红色结点" << endl;
		return false;
	}
	if (root->_col == BLACK)
	{
		count++;
	}
	return _ISRBTree(root->_left, count, BlackCount) 
        && _ISRBTree(root->_right, count, BlackCount);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/20826.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java学习之包访问修饰符

基本介绍 java 提供四种访问控制修饰符号&#xff0c;用于控制方法和属性(成员变量)的访问权限&#xff08;范围&#xff09; 公开级别:用 public 修饰,对外公开受保护级别:用 protected 修饰,对子类和同一个包中的类公开默认级别:没有修饰符号,向同一个包的类公开.私有级别:…

采用sFlow工具实现流量监控--实验

采用sFlow工具实现流量监控--实验采用sFlow工具实现流量监控---实验学习目标学习内容实验原理实验拓扑实验仿真启动sFlow-rt以及floodlight控制器创建拓扑部署sFlow agent步骤1.步骤2.步骤3步骤4步骤5.步骤6.总结申明&#xff1a; 未经许可&#xff0c;禁止以任何形式转载&…

C++模拟OpenGL库——图形学状态机接口封装(一):用状态模式重构部分代码及接口定义

目录 什么是状态机&#xff1f; 基于状态机模式进行重构 Canvas.h源码 什么是状态机&#xff1f; 回顾之前两部分内容&#xff0c;我们做了&#xff1a; 绘制点绘制线&#xff08;Brensenham&#xff09;绘制三角形&#xff08;拆分法&#xff09;图片操作&#xff08;stb…

RabbitMQ------延迟队列(整合SpringBoot以及使用延迟插件实现真正延时)(七)

RabbitMQ------延迟队列&#xff08;七&#xff09; 延迟队列 延迟队列&#xff0c;内部是有序的&#xff0c;特点&#xff1a;延时属性。 简单讲&#xff1a;延时队列是用来存放需要在指定时间被处理的元素队列。 是基于死信队列的消息过期场景。 适用场景 1.订单在十分钟…

Linux(centos7)安装MySQL5.7

Linux 安装MySQL5.7 数据库 所有的安装方式是基于手动式的安装&#xff0c;也就是整体的下载然后配置 rpm与yum之间的关系 rpm 是Linux 免除编译安装带来的安装方式&#xff0c;而yum 是在rpm 上面的进一步的优化&#xff0c;换句话说yum 既包含了rpm 的简单安装&#xff0c…

百度地图自定义覆盖物(html)格式

<style type"text/css"> body, html{ width: 100%; height: 100%; overflow: hidden; margin: 0; font-family: "微软雅黑"; display: flex; justify-content: space-between; } #cont…

使用html+css实现一个静态页面(厦门旅游网站制作6个页面) 旅游网页设计制作 HTML5期末考核大作业,网站——美丽家乡。 学生旅行 游玩 主题住宿网页

&#x1f468;‍&#x1f393;静态网站的编写主要是用 HTML DⅣV CSSJS等来完成页面的排版设计&#x1f469;‍&#x1f393;&#xff0c;一般的网页作业需要融入以下知识点&#xff1a;div布局、浮动定位、高级css、表格、表单及验证、js轮播图、音频视频Fash的应用、uli、下拉…

FL Studio2023水果完整中文版音乐制作软件

FL Studio2023水果中文版是一款由 Image Line 公司研发几近完美的虚拟音乐工作站,同时也是知名的音乐制作软件。它让你的计算机就像是全功能的录音室&#xff0c;漂亮的大混音盘&#xff0c;先进的创作工具&#xff0c;让你的音乐突破想象力的限制。它可以播放由你指定或加入的…

IP包头分析

数据来源 IP包头长度 ip包头的长度在20-60个字节间&#xff0c;一般是20字节&#xff08;固定部分&#xff09;&#xff0c;可选项最大是40个字节&#xff08;比较少用&#xff09;。 第一行 版本 就是指出IP数据包是什么版本&#xff1b;常见的版本就是0100 IPV4和 0110 IPV6…

机器学习中基本符号表示和常用术语

目录一. 基本符号表示二. 常用术语1. 精准率计算&#xff08;precision&#xff09;2.召回率计算&#xff08;recall&#xff09;3.准确率的计算&#xff08;accuracy&#xff09;4.F1 Score5. G分数6.一. 基本符号表示 TP &#xff08;true positive&#xff09;&#xff1a;预…

【Python】基础语法(安装,常变量,类型,注释,运算符)

目录python环境搭建安装Python安装pycharmpython基础语法常量和表达式变量和数据类型变量数据类型注释输入输出运算符算术运算符关系运算符逻辑运算符赋值运算符xdm,最近更新一些学习Python基础知识的内容,感谢支持!python环境搭建 俗话说工欲善其事必先利其器,要想学习Python开…

新知实验室TRTC初体验

小记 一次偶然的邂逅,让我知道了TRTC实时音视频这个神奇的东西,于是便开始研究起来这个鬼东西,本以为是一个很简单的东西,调用一下SDK就完事了 , 谁知道它的文档并不是很齐全,这一点还需要多多努力啊!!! 正文 实时音视频&#xff08;TRTC&#xff09; 是腾讯云提供的一套低…

现代对称密码

乘积密码 因为语言特性&#xff0c;用代替和置换是不安全的&#xff0c;可以考虑用多次的加密增强密码强度。多次加密想要提高密码强度&#xff0c;要求多次加密不能成为一个群&#xff0c;那么加密就可以被重复并且组合复杂度会增加。 分组密码 分组密码就是把明文分组后进…

Linux进阶-Shell编程与环境变量

目录 定义变量&#xff1a; 使用变量&#xff1a; 将命令的结果赋值给变量&#xff1a; 删除变量&#xff1a;unset 退出当前进程&#xff1a;exit 读取从键盘输入的数据 &#xff1a;read 对整数进行数字运算&#xff1a;(()) 逻辑与或&#xff1a; 检测某个条件是否成…

【Java八股文总结】之MySQL数据库

文章目录数据库一、基本概念二、MySQL数据库2.1 MySQL基础1、MySQL数据库的优点&#xff1f;2、MySQL支持的数据类型有&#xff1f;Q&#xff1a;varchar 和 char 的区别&#xff1f;Q&#xff1a;blob 和 text 的区别&#xff1f;Q&#xff1a;datetime 和 timestamp 的区别&a…

DI依赖注入-P8,P9,P10,P11

1.构造器注入 之前写过了~~~~ 2.Set方式注入【重点】 3.拓展方式注入 2.Set方式注入【重点】 【环境搭建】 1.复杂类型 2.真实测试对象 四个文件 Student实体类的创建&#xff1a; 主要是依据官方文档来建立。那个Address也是为了测试不同的类型&#xff0c;而创建的引…

攻防世界misc2-1

misc2-1 题目描述&#xff1a;无 题目环境&#xff1a;https://download.csdn.net/download/m0_59188912/87094620 打开图片&#xff0c;发现无法显示。 使用winhex打开&#xff0c;从其中一段看出这是逆序图片。 使用python脚本将其正序排列。 脚本源码&#xff1a; f1open(‘…

5G无线技术基础自学系列 | SA及NSA组网架构

素材来源&#xff1a;《5G无线网络规划与优化》 一边学习一边整理内容&#xff0c;并与大家分享&#xff0c;侵权即删&#xff0c;谢谢支持&#xff01; 附上汇总贴&#xff1a;5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 3GPP为新空中接口定义了两种部署配置&a…

操作系统笔记

文章目录一、操作系统的定义1.1 操作系统的功能和目标1.2 操作系统的特征1.3 操作系统的发展和分类1.4 操作系统的运行机制1.5 操作系统内核1.6 操作系统的体系结构二、中断机制中断和异常三、系统调用3.1 系统调用的分类&#xff08;按功能分配&#xff09;3.2 系统调用和库函…

整夜我的背影是一条踏往星空的道路

Brigit Pegeen Kelly&#xff0c;1951 - 2016.08.14&#xff0c;美国诗人、教师&#xff0c;在加利福尼亚州帕洛阿尔托出生&#xff0c;在印第安纳南部长达&#xff0c;成年后的大部分时间都在伊利诺州中部度过。一位非常注重隐私的女性&#xff0c;她的生活很少为人所知。[1][…