红黑树与红黑树的插入——用C++实现

article/2025/6/15 18:06:35

一、红黑树简介

        红黑树是二叉搜索树的一种,区别于二叉平衡树,红黑树的平衡并不以平衡因子为依据进行平衡的调整而是以五条规则对红黑树进行定义,从而达成树的最长路径最多是树的最短路径的两倍长。以下是红黑树的五条规则:

  1. 节点非红即黑:每个节点只能是红色或黑色。

  2. 根节点必黑:树的根节点永远是黑色的。

  3. 叶子节点(NULL)为黑:所有叶子节点(空指针,通常用统一的哨兵节点 NULL 表示)都是黑色的。

  4. 红节点之子必黑:如果一个节点是红色的,那么它的两个子节点都必须是黑色的(即不能有两个连续的红色节点)。

  5. 黑高一致:从任意一个节点出发,到达其所有后代叶子节点(NULL)的路径上,包含的黑色节点数量(称为黑高)必须完全相同

        由于第五条,路径上黑色结点数量相同,在加上第四条,不能有两个连续的红色结点,就可以形成红黑树最长路径最多是树的最短路径的两倍长。

        那么红黑树与二叉平衡树相比有什么区别呢?

  • 首先,红黑树由于定义使得左右子树之间的高度差要求没有二叉平衡树那么严格,因此在插入删除时,需要进行旋转的操作会更少一点。因此,在这方面会比二叉平衡树更优。
  • 其次,就是红黑树的查找效率,对比平衡二叉树会稍差一点,由于左右子树之间的差距相比二叉平衡树变大,树高会更高一些。

综合以上两点,红黑树是在牺牲一部分查找效率的情况下,提高的插入和删除效率。且由于红黑树的特征,使红黑树与二叉平衡树之间的查找效率差距并不大,因此,日常使用更多的还是红黑树。

二、红黑树的插入

        对于红黑树的插入有一个可以直观演示的网站,有兴趣可以看看Red/Black Tree Visualization

2.1 如何维护红黑树的规则

        首先是第一条,结点只有红色或者黑色,只需要在结点中增加一个变量,用来记录颜色即可。

        第二条,根结点一定为黑,这就要求我们在调整完插入结点后,注意根结点的颜色是否为黑色。

        第三条,可以用来帮助检查是否为红黑树:记录路径上黑色结点的个数,检查到nullptr,也就是叶子结点时停止计数,即为一条路径上黑色结点的个数。

        第四条,不存在连续的红色结点,因此,在插入时,如果遇到了插入结点和父结点均为红色的情况,需要进行颜色的改变,甚至旋转的操作,使得第四条规则得到维护,具体情况,后面再分析。

        第五条,黑高一致,就需要在为了保证第四条进行改颜色和旋转操作的同时,注意不能改变黑高。同时,这也决定了新插入的结点的颜色,必须为红色,如果新插入的结点是黑色,那么就会影响其他路径,进行维护时代价过大。

2.2 如何维护红红不相邻与黑高一致

        若是新插入结点的父结点为黑色,则不需要进行调整。

        由于根结点必定为黑,因此,如果遇到了红红相邻的情况,说明插入的结点的父结点一定不是根结点,因此父结点一定存在父结点,也就是父结点的父结点。

        而维护的方法,也被总结为,看叔叔结点的颜色,叔叔结点总共有三种情况:不存在、红色、黑色。

2.2.1 叔叔结点为红色

        这个图仅抽象出了一种情况,不论插入结点在父结点的左右,或者父结点在祖父结点的左右,只要叔叔结点是红色,都只需要,将父结点和叔叔结点变为黑色,将祖父结点变为红色即可。

        这样在祖父结点这一棵子树上,不仅能维护第四条,也能维护第五条。但是,调整并不会在这里结束。

        由于祖父结点变为了红色,可能存在祖父结点的父结点为红色的情况,因此,要将祖父结点当成新插入的结点,继续向上调整。 

2.2.2 叔叔结点为黑色

        那么此时,说明结点一定是通过调整以后变成红色,而不是新插入的结点,那么其子树一定存在黑色结点。

        如果父结点是祖父结点的左孩子,当前结点是父结点的左孩子,则右旋,然后修改父结点和祖父结点的颜色。

        如果父结点是祖父结点的右孩子,当前结点是父结点的右孩子,则左旋,然后修改父结点和祖父结点的颜色。

        但是,双旋的情况与单旋不一样,双旋需要修改的是当前结点与祖父结点的颜色。

        综上,根据父结点在祖父结点的子树位置,和当前结点在父结点子树位置,确定是单旋还是双旋。再根据是单旋还是双旋,确定修改颜色的结点。

2.2.3 叔叔结点不存在

        叔叔结点不存在,实质上情况与叔叔结点为黑色的处理方式是一样的,也就是——根据父结点在祖父结点的子树位置,和当前结点在父结点子树位置,确定是单旋还是双旋。再根据是单旋还是双旋,确定修改颜色的结点。

        经过旋转和改色,原先祖父结点位置的颜色仍然是黑色,因此不再需要向上调整,调整结束。

        综上,叔叔结点为红色,则修改父结点、叔叔结点、祖父结点的颜色,然后以祖父结点为当前结点继续向上调整;叔叔结点为黑色,则进行旋转和改色,完成后不再需要向上调整。

以下为插入的代码实现,附带验证函数:

// 枚举值表示颜色
enum Colour
{RED,BLACK
};// 这里我们默认按key/value结构实现
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), _col(RED){}
};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) {parent = cur;if (cur->_kv.first < kv.first) {cur = cur->_right;}else if (cur->_kv.first > kv.first) {cur = cur->_left;}else {return false;}}cur = new Node(kv);cur->_parent = parent;if (parent->_kv.first > kv.first) {parent->_left = cur;}else {parent->_right = cur;}if (parent->_col == BLACK) {return true;}Node* pp = parent->_parent;while (parent && pp) {Node* uncle = nullptr;if (parent == pp->_left) {uncle = pp->_right;}else {uncle = pp->_left;}if (uncle != nullptr && uncle->_col == RED) {parent->_col = BLACK;pp->_col = RED;uncle->_col = BLACK;cur = pp;if (cur->_parent == nullptr) {cur->_col = BLACK;return true;}if (cur->_parent->_col == BLACK) return true;parent = cur->_parent;pp = parent->_parent;}else {if (parent == pp->_left && cur == parent->_left) {RotateR(pp);parent->_col = BLACK;pp->_col = RED;}else if (parent == pp->_left && cur == parent->_right) {RotateLR(pp);cur->_col = BLACK;pp->_col = RED;}else if (parent == pp->_right && cur == parent->_left) {RotateRL(pp);cur->_col = BLACK;pp->_col = RED;}else {RotateL(pp);parent->_col = BLACK;pp->_col = RED;}break;}}while (parent != nullptr && parent->_parent != nullptr) {parent = parent->_parent;}_root = parent;return true;}bool IsBalanceTree(){if (_root == nullptr)return true;if (_root->_col == RED)return false;// 参考值int refNum = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK){++refNum;}cur = cur->_left;}return Check(_root, 0, refNum);}void InOrder() {_InOrder(_root);}private:void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}// 前序递归遍历bool Check(Node* root, int blackNum, const int refNum){if (root == nullptr){// 前序遍历走到空时,意味着一条路径走完了//cout << blackNum << endl;if (refNum != blackNum){cout << "存在黑色结点的数量不相等的路径" << endl;return false;}return true;}// 检查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲就方便多了if (root->_col == RED && root->_parent && root->_parent->_col == RED){cout << root->_kv.first << "存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){blackNum++;}return Check(root->_left, blackNum, refNum)&& Check(root->_right, blackNum, refNum);}// 右单旋void RotateR(Node* pParent) {Node* cur = pParent->_left;pParent->_left = cur->_right;cur->_right = pParent;if (pParent->_left != nullptr) {pParent->_left->_parent = pParent;}cur->_parent = pParent->_parent;pParent->_parent = cur;if (cur->_parent != nullptr) {if (cur->_parent->_left == pParent) {cur->_parent->_left = cur;}else {cur->_parent->_right = cur;}}}// 左单旋void RotateL(Node* pParent) {Node* cur = pParent->_right;pParent->_right = cur->_left;cur->_left = pParent;if (pParent->_right != nullptr) {pParent->_right->_parent = pParent;}cur->_parent = pParent->_parent;pParent->_parent = cur;if (cur->_parent != nullptr) {if (cur->_parent->_left == pParent) {cur->_parent->_left = cur;}else {cur->_parent->_right = cur;}}}// 右左双旋void RotateRL(Node* pParent) {Node* cur = pParent->_right;Node* left = cur->_left;RotateR(cur);RotateL(pParent);}// 左右双旋void RotateLR(Node* pParent) {Node* cur = pParent->_left;Node* right = cur->_right;RotateL(cur);RotateR(pParent);}Node* _root = nullptr;
};


http://www.hkcw.cn/article/IqLpbYIVhw.shtml

相关文章

线程相关面试题

提示&#xff1a;线程相关面试题&#xff0c;持续更新中 文章目录 一、Java线程池1、Java线程池有哪些核心参数&#xff0c;分别有什么的作用&#xff1f;2、线程池有哪些拒绝策略&#xff1f;3、说一说线程池的执行流程?4、线程池核心线程数怎么设置呢&#xff1f;4、Java线程…

Axure设计案例:滑动拼图解锁

设计以直观易懂的操作方式为核心&#xff0c;只需通过简单的滑动动作&#xff0c;将拼图块精准移动至指定位置&#xff0c;即可完成解锁。这种操作模式既符合用户的日常操作习惯&#xff0c;在视觉呈现上&#xff0c;我们精心设计拼图图案&#xff0c;融入生动有趣的元素&#…

报表/报告组件(二)-实例与实现解释

上篇《报表/报告组件(一)-指标/属性组件设计》介绍了组件核心指标/属性设计&#xff0c;本文以实例介绍各个特性的实现和效果&#xff0c;实例是多个报告融合&#xff0c;显示所有的特性。 设计 指标/属性组件是报告/报表关键部分&#xff0c;上篇已介绍过&#xff0c;本节回顾…

django入门-orm数据库操作

一&#xff1a;下载数据库依赖项mysqlclient pip install mysqlclient 二&#xff1a;django配置文件配置数据库链接 路径&#xff1a;mysite2\mysite2\settings.py DATABASES {default: {ENGINE: django.db.backends.mysql,NAME: data, # 数据库名称USER: root, …

开疆智能Profinet转Profibus网关连接CMDF5-8ADe分布式IO配置案例

本案例是客户通过开疆智能研发的Profinet转Profibus网关将PLC的Profinet协议数据转换成IO使用的Profibus协议&#xff0c;操作步骤如下。 配置过程&#xff1a; Profinet一侧设置 1. 打开西门子组态软件进行组态&#xff0c;导入网关在Profinet一侧的GSD文件。 2. 新建项目并…

【RabbitMQ】- Channel和Delivery Tag机制

在 RabbitMQ 的消费者代码中&#xff0c;Channel 和 tag 参数的存在是为了实现消息确认机制&#xff08;Acknowledgment&#xff09;和精细化的消息控制。 Channel 参数 作用 Channel 是 AMQP 协议的核心操作接口&#xff0c;通过它可以直接与 RabbitMQ 交互&#xff1a; 手…

详解代理型RAG与MCP服务器集成

检索增强型生成(RAG)将语言模型与外部知识检索相结合,让模型的回答基于最新的事实,而不仅仅是其训练数据呢。 RAG(高级别) 在 RAG 流程中,用户查询用于搜索知识库(通常通过向量数据库中的嵌入来实现),并将检索到的最相关文档“增强”到模型的提示中,以帮助生成事实…

Keil 中因未引入源文件导致的函数调用与索引失败:从找不到定义到全局搜索无效

我在头文件中声明函数&#xff0c;源文件有定义&#xff0c;在有引入头文件的情况下调用的时候却找不到函数&#xff0c;头文件点击函数跳转不到源文件&#xff0c;全局搜索函数时找不到源文件的这个函数&#xff0c;最后是因为没有引入这个源文件 一、我在MQTT_Client_Task中使…

vue3学习

p3 创建vue3工程 1.创建命令 npm create vuelatest p4 编写APP组件 入口文件是index.html Vite 项目中&#xff0c; index.htm 是项目的入口文件&#xff0c;在项目最外层 加载index.html后&#xff0c;Vite解析 script typemoduleSrCXXX 指向的javascript Vue 中是通过 cr…

Vue3 + Vite:我的 Qiankun 微前端主子应用实践指南

前言 实践文章指南 vue微前端qiankun框架学习到项目实战,基座登录动态菜单及权限控制>>>>实战指南&#xff1a;Vue 2基座 Vue 3 Vite TypeScript微前端架构实现动态菜单与登录共享>>>>构建安全的Vue前后端分离架构&#xff1a;利用长Token与短Tok…

CloudFront 加速详解:AWS CDN 怎么用?

让全球访问更快速稳定&#xff0c;深入解读 AWS 的内容分发网络 在上一篇中&#xff0c;我们介绍了 Amazon S3 对象存储&#xff0c;它非常适合托管静态资源&#xff0c;比如图片、视频、网页等。但你可能遇到过这样的问题&#xff1a; “我把网站静态文件部署到了 S3&#xf…

嵌入式SDK技术EasyRTC音视频实时通话助力即时通信社交/教育等多场景创新应用

一、引言​ 在数字化时代&#xff0c;即时通信已成为人们生活和工作中不可或缺的部分。音视频功能作为即时通信的核心&#xff0c;能实现更加直观、高效的信息传递。EasyRTC作为一款强大的实时通信框架&#xff0c;具备诸多优势&#xff0c;为即时通信的音视频应用提供了优质解…

Rust 学习笔记:关于 Cargo 的练习题

Rust 学习笔记&#xff1a;关于 Cargo 的练习题 Rust 学习笔记&#xff1a;关于 Cargo 的练习题问题一问题二问题三问题四问题五问题六问题七 Rust 学习笔记&#xff1a;关于 Cargo 的练习题 参考视频&#xff1a; https://www.bilibili.com/video/BV1xjAaeAEUzhttps://www.b…

【时时三省】(C语言基础)数组作为函数参数

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 调用有参函数时&#xff0c;需要提供实参。例如sin ( x )&#xff0c;sqrt ( 2&#xff0c;0 )&#xff0c;max ( a&#xff0c;b )等。实参可以是常量、变量或表达式。数组元素的作用与变量…

基于Android的一周穿搭APP的设计与实现 _springboot+vue

开发语言&#xff1a;Java框架&#xff1a;springboot AndroidJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7数据库工具&#xff1a;Navicat12开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;Maven3.6 系统展示 APP登录 A…

【开源工具】超全Emoji工具箱开发实战:Python+PyQt5打造跨平台表情管理神器

&#x1f31f; 超全Emoji工具箱开发实战&#xff1a;PythonPyQt5打造跨平台表情管理神器 &#x1f308; 个人主页&#xff1a;创客白泽 - CSDN博客 &#x1f525; 系列专栏&#xff1a;&#x1f40d;《Python开源项目实战》 &#x1f4a1; 热爱不止于代码&#xff0c;热情源自每…

当 “欧洲版 Cursor” 遇上安全危机

在 AI 编程助手蓬勃发展的当下&#xff0c;安全问题正成为行业不容忽视的隐忧。近期&#xff0c;AI 编程助手公司 Replit 与号称 “欧洲版 Cursor” 的 Lovable 之间&#xff0c;因安全漏洞问题掀起了一场风波&#xff0c;引发了业界的广泛关注。​ Replit 的员工 Matt Palmer…

Agentic Workflow是什么?Agentic Workflow会成为下一个AI风口吗?

无论是想要学习人工智能当做主业营收&#xff0c;还是像我一样作为开发工程师但依然要运用这个颠覆开发的时代宠儿&#xff0c;都有必要了解、学习一下人工智能。 近期发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;入行门槛低&#x…

遥感影像建筑物变化检测

文章目录 效果1、环境安装2、项目下载3、数据集下载4、模型训练5、模型推理6、推理结果7、批量推理效果 1、环境安装 参考文章 搭建Pytorch的GPU环境超详细 win10安装3DGS环境(GPU)超详细 测试GPU环境可用 2、项目下载 https://gitcode.com/gh_mirrors/ch/change_detectio…