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

【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.dtcms.com/a/363534.html

相关文章:

  • 嵌入式C语言之链表冒泡排序
  • Java基础第9天总结(可变参数、Collections、斗地主)
  • 深入浅出数据库事务:从原理到实践,解决 Spring 事务与外部进程冲突问题
  • github下载的文件内容类似文件哈希和存储路径原因
  • Kafka 分层存储(Tiered Storage)从 0 到 1 的配置、调优与避坑
  • Vue3 实现自定义指令点击空白区域关闭下拉框
  • 【51单片机】【protues仿真】 基于51单片机智能电子秤系统
  • 工业界实战之数据存储格式与精度
  • 嵌入式解谜日志-网络编程
  • 浏览器面试题及详细答案 88道(56-66)
  • MySQL查询limit 0,100和limit 10000000,100有什么区别?
  • 敏捷规模化管理工具实战指南:如何实现跨团队依赖可视化?
  • 数据库驱动改造加密姓名手机号证件号邮箱敏感信息
  • web自动化测试(selenium)
  • RK-Android15-WIFI白名单功能实现
  • 一次别开生面的Java面试
  • Servlet基础
  • Redisson分布式锁会发生死锁问题吗?怎么发生的?
  • Aurobay EDI 需求分析:OFTP2 与 EDIFACT 驱动的汽车供应链数字化
  • UniApp 实现搜索页逻辑详解
  • uniapp H5预览图片组件
  • 年轻教师开学焦虑破解:从心出发,重构健康工作生活新秩序
  • uni-app 布局之 Flex
  • 第一讲、Kafka 初识与环境搭建
  • ImageMagick命令行图片工具:批量实现格式转换与压缩,支持水印添加及GIF动态图合成
  • windows系统离线安装Ollama、创建模型(不使用docker)、coze调用
  • 51c大模型~合集177
  • Swift 解法详解:LeetCode 369《给单链表加一》
  • 研发团队缺乏统一文档模板怎么办
  • 苹果开发中什么是Storyboard?object-c 和swiftui 以及Storyboard到底有什么关系以及逻辑?优雅草卓伊凡