【C++】红黑树(详解)

文章目录

  • 上文链接
  • 一、什么是红黑树
  • 二、红黑树的性质
    • 1. 颜色规则
    • 2. 红黑树的规则为什么可以控制平衡
    • 3. 红黑树的效率
  • 三、红黑树的整体结构
  • 四、红黑树的插入
    • 1. 空树的插入
    • 2. 插入节点的父亲为黑色
    • 3. 插入节点的父亲为红色
      • (1) 叔叔为红色:变色
      • (2) 叔叔为空或为黑色:旋转 + 变色
        • ① LL 型:右单旋 + 变色
        • ② RR 型:左单旋 + 变色
        • ③ LR 型:左右双旋 + 变色
        • ④ RL 型:右左双旋 + 变色
    • 4. 代码部分
  • 五、红黑树的查找与删除
  • 六、红黑树的平衡验证

上文链接

  • 【C++】AVL树(详解)

一、什么是红黑树

我们之前学习过了二叉搜索树和 AVL 树,红黑树和 AVL 树一样,也是一棵自平衡的二叉搜索树,AVL 树是通过控制左右子树的高度差不超过 1 来保持平衡,而红黑树则是为每个结点增加一个存储位来表示结点的颜色,可以是红色或者黑色。 通过对任何一条从根到叶子的路径上各个结点的颜色进行约束来保持树的左右两边的相对平衡。红黑树确保没有一条路径会比其他路径长出 2 倍,即 2 * 最短路径 >= 最长路径,因而是接近平衡的。

如下图所示就是一棵红黑树:

请添加图片描述


二、红黑树的性质

1. 颜色规则

红黑树为何能够确保没有一条路径会比其他路径长出 2 倍呢?是因为它有如下四个规则:

  1. 每个结点不是红色就是黑色;
  2. 根结点是黑色的;
  3. 如果一个结点是红色的,则它的两个孩子结点必须是黑色的,也就是说任意一条路径不会有连续的红色结点;
  4. 对于任意一个结点,从该结点到其所有空结点的简单路径上,均包含相同数量的黑色结点。

请添加图片描述

说明:《算法导论》等书籍上补充了一条每个叶子结点 (NIL) 都是黑色的规则。他这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把 NIL 叫做外部结点。NIL 是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了 NIL 结点,所以我们知道一下这个概念即可。

请添加图片描述

有了上面的描述,我们可以总结出红黑树的性质:

  • 左根右:由于它是二叉搜索树,所以左子树的值 < 根 < 右子树的值;
  • 根叶黑:根节点和叶子节点 (NIL) 都是黑色的;
  • 不红红:一条路径上不会出现两个连续的红色节点;
  • 黑路同:任意两条路径上的黑色节点个数相同。

因此,红黑树的性质可以简记为:左根右,根叶黑,不红红,黑路同


2. 红黑树的规则为什么可以控制平衡

为什么有了上面四条规则就可以确保红黑树中没有一条路径会比其他路径长出 2 倍呢?

黑路同可知,从根到空结点的每条路径都有相同数量的黑色结点,所以极端场景下,最短路径就是全为黑色结点的路径,假设最短路径长度为 h。

又由不红红可知,任意一条路径不会有连续的红色结点,所以极端场景下,最长的路径就是一黑一红间隔组成,那么最长路径的长度就为 2 * h。

综上所述,在最极端的情况下,红黑树中的最长路径也才刚好是最短路径的两倍,所以对于任意一棵红黑树来说 2 * 最短路径 >= 最长路径,因此没有一条路径会比其他路径长出 2 倍。


3. 红黑树的效率

请添加图片描述
黑树与 AVL 树的增删查改的效率都是在同一个档次,时间复杂度都是 O(log⁡N)O(\operatorname{log}N)O(logN)

在插入和删除方面,由于 AVL 树的平衡条件相对严格一些,因此需要频繁地进行旋转操作,效率比红黑树略低;

在查找方面,红黑树不像 AVL 树那样保持严格的平衡,因此在查找时的效率会比 AVL 树略低。


三、红黑树的整体结构

#pragma once
// 枚举值表示颜色 
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){}
};template<class K, class V>
class RBTree
{typedef RBTreeNode<K, V> Node;
public:// ...private:Node* _root = nullptr;
};

四、红黑树的插入

1. 空树的插入

当我们对一棵空的红黑树进行插入时,直接插入并把其颜色设置为黑色即可。

(下面所讨论的插入操作都是建立在树非空的情况之上)


2. 插入节点的父亲为黑色

首先我们需要明确一点,在红黑树非空的情况下,我们插入一个节点的颜色必定是红色

这是因为如果我们把插入节点的颜色设置为黑色的话,那么此时这棵红黑树就必然会违反黑路同的性质,之后想要把每条路上的黑色节点数量调整为一样多的话就会非常麻烦。所以从这个角度来看,树非空的情况下我们插入的节点都设置为红色

此时如果它的父亲是黑色,那么将不会违反红黑树的任何一条性质,那么插入结束。


3. 插入节点的父亲为红色

(1) 叔叔为红色:变色

我们插入的节点为红色,此时如果它的父亲也为红色,那么就会违反不红红的性质,那么此时必然我们会让父亲变为黑色,但这也意味着父亲这条路上多了一个黑色节点,违反了黑路同,我们需要调节黑色节点的数量。此时我们可以让父亲的父亲 (爷爷) 变为红色 (由于父亲为红,所以爷爷变之前必然为黑),再让叔叔节点变为黑色,这样一来,我们就可以满足不红红黑路同两个性质了。

但是,此时由于爷爷变红,可能又会导致出现两个红色节点相邻的情况,因为爷爷的父亲可能为红色。此时我们只需要把爷爷当作新插入的节点,重复上面的操作即可。下面是图片演示:

注:下图中用 c (cur) 表示插入节点,用 p (parent) 表示父节点,用 g (grandparent) 表示爷爷节点,用 u (uncle) 表示叔叔节点。

请添加图片描述

一直重复这样的操作,直到不违反红黑树的性质或者到根为止。注意到根的时候需要把根变为黑色


(2) 叔叔为空或为黑色:旋转 + 变色

① LL 型:右单旋 + 变色

父亲节点为红色,叔叔节点为空或为黑色时,我们需要先进行旋转操作。注意前面我们说过空节点 (NIL) 一定是黑色,所以为空或者为黑色其实是一种情况。具体怎么旋转呢?

如果此时插入节点的父亲节点在爷爷节点的左边 (left),插入节点在父亲节点的左边 (left),那么我们把这种情况成为 LL 型。如果是 LL 型,那么需要将以爷爷节点为根的子树进行右单旋操作,旋转之后将爷爷和父亲节点进行变色 (红变黑,黑变红)。这样操作之后,将不会违反红黑树的性质,插入结束。

请添加图片描述

这种旋转 + 变色的操作不仅会出现在单纯的插入过程中,也有可能出现在我们上面讲的情况 (1) 的过程中。即在情况 (1) 的向上变色更新过程中发现 cur 的叔叔颜色为黑就需要进行旋转 + 变色。

请添加图片描述


② RR 型:左单旋 + 变色

如果此时插入节点的父亲节点在爷爷节点的右边 (right),插入节点在父亲节点的右边 (right),那么我们把这种情况成为 RR 型。如果是 RR 型,那么需要将以爷爷节点为根的子树进行左单旋操作,旋转之后将爷爷和父亲节点进行变色 (红变黑,黑变红)。这样操作之后,将不会违反红黑树的性质,插入结束。

可以看到这种情况与上面的 LL 型几乎一样,就是一个对称的关系,这里就不画图演示了。


③ LR 型:左右双旋 + 变色

如果此时插入节点的父亲节点在爷爷节点的左边 (left),插入节点在父亲节点的右边 (right),那么我们把这种情况成为 LR 型。如果是 LR 型,那么需要将以爷爷节点为根的子树进行左右双旋操作,旋转之后将 cur 和爷爷节点进行变色,插入结束。

请添加图片描述


④ RL 型:右左双旋 + 变色

如果此时插入节点的父亲节点在爷爷节点的右边 (right),插入节点在父亲节点的左边 (left),那么我们把这种情况成为 RL 型。如果是 RL 型,那么需要将以爷爷节点为根的子树进行右左双旋操作,旋转之后将 cur 和爷爷节点进行变色,插入结束。

这种情况与 LR 型完全对称。


4. 代码部分

// 旋转代码的实现跟 AVL 树非常类似的,只是不需要更新平衡因子
bool Insert(const pair<K, V>& kv)
{// 如果是空树那么直接让插入节点变为黑if (_root == nullptr){_root = new Node(kv);_root->_col = BLACK;return true;}Node* parent = nullptr;Node* cur = _root;// 走正常二叉搜索树的插入逻辑,找到插入位置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 (parent == grandfather->_left){Node* uncle = grandfather->_right;// 如果叔叔节点为红色,那么进行变色处理if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;cur = grandfather;  // 更新 cur 继续向上处理parent = cur->_parent;}// 如果叔叔为黑色或者为空,那么需要旋转 + 变色else{// 如果插入在父亲节点的左边,此时为 LL 型if (cur == parent->_left){// 右单旋 + 变色RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}// LR 型else{// 左右双旋 + 变色RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}// 旋转 + 变色操作完是一定符合红黑树的性质的,因此直接结束更新break;}}// 父亲节点在爷爷节点的右边else{Node* uncle = grandfather->_left;// 叔叔节点为红色,变色即可if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;// 继续往上处理 cur = grandfather;parent = cur->_parent;}// 叔叔为黑色或为空else{// RR 型if (cur == parent->_right){// 左单旋 + 变色RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}// RL 型else{// 右左双旋 + 变色RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}// 停止更新break;}}}// 变色更新到根节点时有可能根为红色,需要将根变为黑色_root->_col = BLACK;return true;
}

五、红黑树的查找与删除

红黑树的查找与正常二叉搜索树的查找逻辑一样。

Node* Find(const K& key)
{Node* cur = _root;while (cur){if (cur->_kv.first < key) cur = cur->_right;else if (cur->_kv.first > key) cur = cur->_left;else return cur;}return nullptr;
}

红黑树的删除操作这里不做重点讲解,这个操作会比插入稍复杂一些,具体操作可以参考《算法导论》或者《STL源码剖析》中的讲解。


六、红黑树的平衡验证

如何判断一棵红黑树是否合格呢?这里获取最长路径和最短路径,检查最长路径不超过最短路径的 2 倍是不可行的,因为就算满足这个条 件,红黑树也可能颜色不满足规则,当前暂时没出问题,后续继续插入还是会出问题的。所以我们还是去检查 4 点规则,满足这 4 点规则,一定能保证最长路径不超过最短路径的 2 倍。

  1. 枚举颜色类型,保证颜色不是黑色就是红色;
  2. 检查根的颜色是否为黑色即可;
  3. 不红红:前序遍历检查,请添加图片描述
    遇到红色结点查孩子是否是红色这样不太方便,因为孩子有两个,且不一定存在,但是反过来检查父亲的颜色就方便多了;
  4. 黑路同:前序遍历,遍历过程中用形参记录根到当前节点路径的 blackNum (黑色结点数量),前序遍历遇到黑色结点就 ++blackNum,走到空就计算出了一条路径的黑色结点数量。再用任意一条路径黑色结点数量作为参考值,依次比较即可。
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->_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);
}bool IsBalance()
{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);
}

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

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

相关文章

AI提升SEO关键词效果新策略

内容概要 在2025年&#xff0c;人工智能&#xff08;AI&#xff09;技术正全面革新搜索引擎优化&#xff08;SEO&#xff09;的关键词优化模式。通过智能分析用户搜索意图与语义关联&#xff0c;AI能够精准匹配关键词并进行高效布局。本文将深入探讨AI驱动的关键词策略升级方案…

手动安装的node到nvm吧版本管理的过程。

前言 本文记录个人在使用nvm包管理器安装node 14版本 npm安装失败&#xff0c;进行手动安装的node到nvm吧版本管理的过程。 安装node 14 时 npm总是安装失败&#xff0c;如下图 通过手动下载对于版本 node下载地址 下载解压点击所需的版本下载后解压 修改解压后的文件夹名称…

Python爬虫实战:构建Widgets 小组件数据采集和分析系统

1. 引言 1.1 研究背景 在当今数字化时代,Widgets 作为用户界面的基本组成元素,广泛应用于移动应用、网站和桌面软件中,其设计质量直接影响用户体验。随着市场竞争的加剧,了解市场上各类 Widgets 产品的特征、价格区间、用户评价等信息,对于产品设计和商业决策具有重要价…

1.1 Internet简介

1.网络, 计算机网络, 互联网 2.不同的角度认识Internet1.网络, 计算机网络, 互联网 网络表示连接两点以上的通路系统比如:a.你家到邻居家的小路 -> 一个小网络b.一个村子的所有道路 -> 一个更大的网络c.送外卖的小哥骑车走的路线 -> 一个配送网络计算机网络表示专门传…

pytest使用allure测试报告

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 选用的项目为Selenium自动化测试Pytest框架实战&#xff0c;在这个项目的基础上说allure报告。 allure安装 首先安装python的allure-pytest包 pip install allu…

PortSwigger靶场之SQL injection with filter bypass via XML encoding通关秘籍

一、题目分析该实验室的库存查询功能存在 SQL 注入漏洞。查询结果为这些信息会出现在应用程序的响应中&#xff0c;因此您可以利用“联合”攻击来从其他表中获取数据。该数据库中有一个“用户”表&#xff0c;该表包含了已注册用户的用户名和密码。要解决&#xff0c;需进行一次…

Cocos游戏中自定义按钮组件(BtnEventComponent)的详细分析与实现

概述在Cocos游戏开发中&#xff0c;按钮交互是用户界面中最基础且重要的组成部分。原生的Cocos Button组件虽然功能完善&#xff0c;但在复杂的游戏场景中往往无法满足多样化的交互需求。本文将详细分析一个功能强大的按钮组件BtnEventComponent&#xff0c;它提供了多种交互模…

终于完成William F. Egan所著的Practical RF System Design的中文版学习资料

终于完成William F. Egan所著的Practical RF System Design的中文版学习资料 该文档聚焦RF系统的分析与设计。书中先介绍系统设计流程、书籍结构及工具&#xff08;如电子表格、测试与仿真&#xff09;&#xff0c;接着围绕RF系统关键参数展开&#xff1a;讲解增益计算&#xf…

嵌入式Linux驱动开发:蜂鸣器驱动

嵌入式Linux驱动开发&#xff1a;蜂鸣器驱动 1. 引言 本文档详细记录了基于i.MX6ULL处理器的蜂鸣器驱动开发过程。内容涵盖驱动的理论基础、代码实现、设备树配置以及用户空间应用程序的编写。本文档严格遵循用户提供的代码和文档&#xff0c;确保理论与实践的紧密结合。本文档…

Qt中的锁和条件变量和信号量

Qt中的锁和条件变量和信号量 C11中引入智能指针用来解决锁忘记释放的问题 代码如下&#xff1a; void Thread::run() {for(int i0;i<50000;i){QMutexLocker locker(&mutex);//mutex.lock();num;//mutex.unlock();} }大括号结束的时候&#xff0c;生命周期踩结束&#xf…

智能电视MaxHub恢复系统

公司的MaxHub智能电视又出故障了。 去年硬件故障返厂&#xff0c;花了8600多元。 这次看上去是软件故障。开机后蓝屏报错。 按回车键&#xff0c;电视重启。 反复折腾几次&#xff0c;自行修复执行完毕&#xff0c;终于可以进入系统了。 只不过进入windows10后&#xff0c;图…

TensorFlow 面试题及详细答案 120道(71-80)-- 性能优化与调试

《前后端面试题》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux… 。 前后端面试题-专栏总目录 文章目录 一、本文面试题目录 71. 如何优化TensorFlow模…

数据结构 第三轮

以看严蔚敏老师的教材为主&#xff0c;辅以其他辅导书&#xff1a;王道&#xff0c;新编数据结构&#xff0c;学校讲义 线性结构&#xff1a;线性表、串、队列、栈、数组和广义表 树形结构、网状结构&#xff1a;图 查找、排序 动态内存管理和文件 绪论 8-29 数据&#xf…

[新启航]新启航激光频率梳 “光量子透视”:2μm 精度破除遮挡,完成 130mm 深孔 3D 建模

摘要&#xff1a;本文介绍新启航激光频率梳的 “光量子透视” 技术&#xff0c;该技术凭借独特的光量子特性与测量原理&#xff0c;以 2μm 精度破除深孔遮挡&#xff0c;成功完成 130mm 深孔的 3D 建模&#xff0c;为深孔三维形态的精确获取提供了创新解决方案&#xff0c;推动…

MongoDB /redis/mysql 界面化的数据查看页面App

MongoDB、Redis 和 MySQL 都有界面化的数据查看工具&#xff0c;以下是相关介绍&#xff1a; MongoDB 输入MongoDB的账号密码即可读取数据&#xff0c;可访问数据。 MongoDB Compass&#xff1a;这是 MongoDB 官方提供的 GUI 管理工具&#xff0c;支持 Windows、Mac 和 Linux 等…

Spring Boot 实战:接入 DeepSeek API 实现问卷文本优化

本文结合 Spring Boot 项目&#xff0c;介绍如何接入 DeepSeek API&#xff0c;自动优化问卷文本&#xff0c;并给出完整示例代码及详细注释。一、项目目标 目标是实现一个 REST 接口&#xff0c;将原始问卷文本提交给 DeepSeek API&#xff0c;然后返回优化后的文本给前端。 接…

opencv实现轮廓绘制和选择

前面学习了opencv中图像的一些处理&#xff0c;但对于opencv我们更多的还是对图像做出一些判断和识别&#xff0c;所以下面开始学习图像的识别。 原图&#xff1a; 一 图像轮廓的识别 import cv2 pencv2.imread(pen.png,0) ret,new_pencv2.threshold(pen,120,255,cv2.THRESH_…

【Linux】Docker洞察:掌握docker inspect命令与Go模板技巧

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;CSDN博客专家   &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01…

知料觅得-新一代AI搜索引擎

本文转载自&#xff1a;知料觅得-新一代AI搜索引擎 - Hello123工具导航 ** 一、&#x1f50d; 初识知料觅得&#xff1a;你的 AI 搜索新伙伴 知料觅得是一款融合了前沿人工智能技术的智能搜索引擎&#xff0c;它旨在彻底改变我们获取信息的方式。不同于传统搜索引擎只给你一堆…

高性能网络转发中的哈希表技术选型与实践

引言 在现代网络编程中,处理大量并发连接是一个常见而重要的挑战。特别是在中间件、代理服务器和负载均衡器等场景中,如何高效地管理数万个并发连接并实现数据转发,对系统性能有着至关重要的影响。本文将围绕一个具体的网络转发场景,深入探讨三种不同的哈希表实现(hsearc…