当前位置: 首页 > ops >正文

【C++游记】红黑树

 

枫の个人主页

你不能改变过去,但你可以改变未来

算法/C++/数据结构/C

Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记

 话不多说,让我们一起进入今天的学习吧~~~  

目录

一、红黑树的概念

1.1 红黑树的规则

1.2 红黑树如何确保最长路径不超过最短路径的 2 倍?

1.3 红黑树的效率

二、红黑树的实现

2.1 红黑树的结构

2.2 红黑树的插入

插入过程概述

情况 1:变色处理

情况 2:单旋 + 变色

情况 3:双旋 + 变色

插入代码实现

2.3 红黑树的查找

2.4 红黑树的验证

三、总结

 四、结语


一、红黑树的概念

红黑树是一种二叉搜索树,它的每个结点增加了一个存储位来表示结点的颜色(红色或黑色)。通过对从根到叶子的路径上各个结点的颜色进行约束,红黑树确保没有一条路径会比其他路径长出 2 倍,从而实现了接近平衡的状态。

1.1 红黑树的规则

  • 每个结点不是红色就是黑色
  • 根结点是黑色的
  • 如果一个结点是红色的,则它的两个孩子结点必须是黑色的(即任意一条路径不会有连续的红色结点)
  • 对于任意一个结点,从该结点到其所有 NULL 结点的简单路径上,均包含相同数量的黑色结点

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

1.2 红黑树如何确保最长路径不超过最短路径的 2 倍?

由规则 4 可知,从根到 NULL 结点的每条路径都有相同数量的黑色结点。我们将这个数量称为黑色高度(bh)。

  • 最短路径就是全是黑色结点的路径,长度为 bh
  • 由规则 2 和规则 3 可知,任意一条路径不会有连续的红色结点,因此最长的路径就是一黑一红间隔组成,长度为 2*bh
  • 综合来看,任意一条从根到 NULL 结点路径的长度 h 满足:bh≤h≤2∗bh

1.3 红黑树的效率

假设红黑树中结点数量为 N,最短路径的长度为 h,那么有:2^h−1≤N<2^(2*h)−1,由此可推出h≈logN。这意味着红黑树的增删查改操作最坏情况下的时间复杂度为O(logN)。

与 AVL 树相比,红黑树的表达相对抽象一些。AVL 树通过高度差直观地控制平衡,而红黑树通过颜色约束间接实现近似平衡。两者效率处于同一档次,但插入相同数量的结点时,红黑树的旋转次数更少,因为它对平衡的控制没那么严格。

二、红黑树的实现

2.1 红黑树的结构

首先定义结点的颜色和结构:

// 枚举值表示颜色
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:// 成员函数将在后面实现
private:Node* _root = nullptr;
};

2.2 红黑树的插入

插入过程概述
  1. 按二叉搜索树规则插入新结点
  2. 若为空树,新增结点为黑色;若非空树,新增结点为红色(避免破坏规则 4)
  3. 若父亲结点是黑色,插入结束;若父亲结点是红色(违反规则 3),则需要根据叔叔结点的情况进行处理

说明:新增结点标识为 c (cur),c 的父亲标识为 p (parent),p 的父亲标识为 g (grandfather),p 的兄弟标识为 u (uncle)

情况 1:变色处理

当 c 为红,p 为红,g 为黑,u 存在且为红时:

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

分析:p 和 u 都是红色,g 是黑色,将 p 和 u 变黑使左边子树路径各增加一个黑色结点,g 变红保持了 g 所在子树的黑色结点数量不变,同时解决了 c 和 p 连续红色的问题。需要继续往上更新是因为 g 变为红色后,若 g 的父亲也是红色,则仍违反规则 3。

这种情况只需要变色,不需要旋转,无论 c 是 p 的左孩子还是右孩子,p 是 g 的左孩子还是右孩子,处理方式都相同。

情况 2:单旋 + 变色

当 c 为红,p 为红,g 为黑,u 不存在或 u 存在且为黑时:

  • 若 p 是 g 的左孩子,c 是 p 的左孩子:以 g 为旋转点进行右单旋,然后将 p 变黑,g 变红
  • 若 p 是 g 的右孩子,c 是 p 的右孩子:以 g 为旋转点进行左单旋,然后将 p 变黑,g 变红

分析:此时单纯变色无法解决问题,需要旋转 + 变色。旋转后 p 成为新的根,子树黑色结点的数量不变,且没有连续的红色结点,不需要继续往上更新。

情况 3:双旋 + 变色

当 c 为红,p 为红,g 为黑,u 不存在或 u 存在且为黑时:

  • 若 p 是 g 的左孩子,c 是 p 的右孩子:先以 p 为旋转点进行左单旋,再以 g 为旋转点进行右单旋,然后将 c 变黑,g 变红
  • 若 p 是 g 的右孩子,c 是 p 的左孩子:先以 p 为旋转点进行右单旋,再以 g 为旋转点进行左单旋,然后将 c 变黑,g 变红

分析:这种情况是 c 和 p 不在同一条直线上,需要先通过一次旋转将其变为直线,再进行另一次旋转,最后进行变色处理。处理后 c 成为新的根,子树黑色结点的数量不变,且没有连续的红色结点,不需要继续往上更新。

插入代码实现
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){// 情况1:叔叔存在且为红,变色处理parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续往上处理cur = grandfather;parent = cur->_parent;}else{// 情况2和3:叔叔不存在或为黑,旋转+变色if (cur == parent->_left){// 单旋(右单旋)RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// 双旋(先左后右)RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;  // 旋转后不需要继续往上处理}}else{Node* uncle = grandfather->_left;if (uncle && uncle->_col == RED){// 情况1:叔叔存在且为红,变色处理parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续往上处理cur = grandfather;parent = cur->_parent;}else{// 情况2和3:叔叔不存在或为黑,旋转+变色if (cur == parent->_right){// 单旋(左单旋)RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{// 双旋(先右后左)RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;  // 旋转后不需要继续往上处理}}}// 确保根节点始终为黑色_root->_col = BLACK;return true;
}// 右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;subL->_right = parent;Node* pParent = parent->_parent;parent->_parent = subL;if (pParent == nullptr){_root = subL;}else{if (pParent->_left == parent)pParent->_left = subL;elsepParent->_right = subL;}subL->_parent = pParent;
}// 左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;subR->_left = parent;Node* pParent = parent->_parent;parent->_parent = subR;if (pParent == nullptr){_root = subR;}else{if (pParent->_left == parent)pParent->_left = subR;elsepParent->_right = subR;}subR->_parent = pParent;
}

2.3 红黑树的查找

红黑树的查找操作与普通二叉搜索树相同,时间复杂度为O(logN):

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;
}

2.4 红黑树的验证

红黑树的验证需要检查是否满足前面提到的 4 条规则:

bool Check(Node* root, int blackNum, const int refNum)
{if (root == nullptr){// 到达空节点,检查黑色节点数量是否与参考值相等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);
}

三、总结

红黑树是一种高效的自平衡二叉搜索树,通过颜色约束实现了近似平衡,确保了增删查改操作的时间复杂度为O(logN)。与 AVL 树相比,红黑树在插入操作时旋转次数更少,因此在频繁插入的场景下表现更优。

红黑树的核心是维护其四条规则,通过变色和旋转两种操作来实现。插入操作需要根据叔叔节点的情况分为三种处理方式,而验证操作则需要检查是否满足所有规则。

 四、结语

今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~

http://www.xdnf.cn/news/19501.html

相关文章:

  • Lombok 实用注解深度解析!
  • 【项目】多模态RAG—本地部署MinerU实现多类文档解析
  • 懒加载详细讲解
  • 使用修改过的arj源码编译和测试
  • C++ 学习与 CLion 使用:(五)数据类型,包括整型、实型、字符型、转义字符、字符串、布尔型
  • 从DevOps到BizDevOps:哪些DevOps工具能够成为业务创新加速引擎?
  • 响应式编程框架Reactor【8】
  • Notepad++近期版本避雷
  • 中心扩展算法
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘tox’问题
  • 利用 DrissionPage 精准获取淘宝商品描述:Python 爬虫实战指南
  • C/C++、Python和Java语言的比较
  • 【职业】算法与数据结构专题
  • 15693协议ICODE SLI 系列标签应用场景说明及读、写、密钥认证操作Qt c++源码,支持统信、麒麟等国产Linux系统
  • 浪潮科技Java开发面试题及参考答案(120道题-上)
  • 利用本地电脑上的MobaXterm连接虚拟机上的Ubuntu
  • 基于SpringBoot音乐翻唱平台
  • Linux Shell 脚本中括号类型及用途
  • three.js+WebGL踩坑经验合集(10.2):镜像问题又一坑——THREE.InstancedMesh的正反面向光问题
  • UART-TCP双向桥接服务
  • 【51单片机三路抢答器定时器1工作1外部中断1】2022-11-24
  • 参数检验vs非参数检验
  • docker 网络配置
  • 【高级】系统架构师 | 2025年上半年综合真题
  • 硬件开发_基于Zigee组网的果园养殖监控系统
  • 56_基于深度学习的X光安检危险物品检测系统(yolo11、yolov8、yolov5+UI界面+Python项目源码+模型+标注好的数据集)
  • aws上创建jenkins
  • 力扣 23 912题(堆)
  • JAVA 面试宝典02
  • 工业飞拍技术:高速生产线的 “动态抓拍神器”,到底牛在哪?