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

解读红黑树:揭晓高效数据结构的核心引擎

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

 一.红黑树的概念

红黑树是一颗二叉搜索树,他的每个节点增加一个储存位来表示节点的颜色,可以是红色或者黑色。通过对任何一条从根到叶子的路径上各个节点的颜色进行约束,红黑树确保了没有一条路径会比其他路径长出2倍,因而是接近平衡的。

那接下我们思考一个问题:为什么有了AVL树还要引进红黑树呢?

二.为什么引入红黑树?

我们接下来将AVL树和红黑树进行对比学习。

AVL树和红黑树是两种经典的平衡二叉搜索树,但他们的设计目标和适用场景有所不同。理解他们的异同,能帮助我们更好地选择合适的数据结构。

相似之处:

1.自平衡性:

两者都通过特定的规则(AVL树的平衡因子,红黑树的颜色规则)在插入/删除后自动调整结构,避免退化成链表,保证操作效率。

2.时间复杂度:

插入,删除,查找的最坏时间复杂度均为O(long n),适合适合高效动态数据管理。

3.基于旋转的调整:

均通过左旋/右旋操作恢复平衡,旋转逻辑相似(如单旋,双旋)。

核心区别:

为什么选择红黑树?

1.写入密集型场景更高效

红黑树在插入/删除时调整次数较少,适合需要频繁修改数据的场景(如数据库索引,内存分配器)。

2.实际性能优势

虽然AVL树的查找稍快,但红黑树的综合读写性能更优。例如,Java的TreeMap,C++STL的map均采用红黑树,因其在混合操作中表现更稳定。

3.工程权衡

红黑树的近似平衡在大多数场景下足够高效,同时减少了维护平衡的开销,更适合复杂的应用。

三.红黑树的规则

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

提示:这棵树从根节点到NULL节点一共有9条路径,注意我标红的的提示,到NULL节点位置算一条。 

说明:《算法导论》等书籍上补充了一条每个叶子结点(NIL)都是黑色的规则。他这里所指的叶子结点不是传统的意义上的叶子结点,而是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了方便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道一下这个概念即可。
这样的话就能很直观的看出从某一节点到NULL节点的路径了。
大家还记得我们刚在介绍红黑树的概念时说的吗?红黑树的颜色规则,确保了没有一条路径会比其他路径长出2倍,因而是接近平衡的。那我们来思考一下,这是为什么呢?
• 由规则4可知,从根到NULL结点的每条路径都有相同数量的黑色节点,所以极端场景下,最短路径就是全是黑色节点的路径,假设最短路径长度为bh(black height)

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

• 综合红黑树的4点规则而言,理论上的全黑最短路径和一黑一红的最长路径并不是在每棵红黑树都存在的。假设任意⼀条从根到NULL结点路径的长度为x,那么bh <= x <= 2*bh。

四.红黑树的效率

假设N是红黑树树中结点数量,h最短路径的长度,那么2^h-1<=N<2^2*h - 1 , 由此推出 h ≈ logN ,也就是意味着红黑树增删查改最坏也就是走最长路径2 ∗ logN ,那么时间复杂度还是O(logN) 。

红黑树的表达相对AVL树要抽象一些,AVL树通过高度差直观的控制了平衡。红黑树通过4条规则的颜色约束,间接的实现了近似平衡,他们效率都是同一档次,但是相对而言,插入相同数量的结点,红黑树的旋转次数是更少的,因为他对平衡的控制没那么严格。

五.红黑树的实现

5.1红黑树的结构

//RBTree.henum 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;
};

5.2红黑树的插入

1.插入一个值按二叉搜索树规则进行插入,插入后我们只需要观察是否符合红黑树的4条规则。

2.如果是空树插入,新增结点是黑色结点。如果是非空树插入,新增结点必须红色结点,因为非空树插入,新增黑色结点就破坏了规则4,规则4是很难维护的。

3. 非空树插入后,新增结点必须红色结点,如果父亲结点是黑色的,则没有违反任何规则,插入结束

4. 非空树插入后,新增结点必须红色结点,如果父亲结点是红色的,则违反规则3。进⼀步分析,c是红色,p为红,g必为黑,这三个颜⾊都固定了,关键的变化看u的情况,需要根据u分为以下几种情况具体处理。

说明:下图中假设我们把新增结点标识为c (cur),c的父亲标识为p(parent),p的父亲标识为g(grandfather),p的兄弟标识为u(uncle)。

5.2.1变色

c为红,p为红,g为黑,u存在且为红,则将p和u变黑,g变红。在把g当做新的c,继续往上更新。

分析:因为p和u都是红,g是黑色,把p和u变黑(其实不论是那种情况第一步都是将p变黑),左边子树路径各增加⼀个黑色结点,g再变红,相当于保持g所在子树的黑色结点的数量不变,同时解决了c和p连续红色结点的问题,需要继续往上更新是因为,g是红色,如果g的父亲还是红色,那么就还需要继续处理;如果g的父亲是黑色,则处理结束了;如果g就是整棵树的根,再把g变回黑色。

情况1只变色,不旋转。所以无论c是p的左还是右,p是g的左还是右,都是上面的变色处理方式。

5.2.2单旋+变色

c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变变将c从黑色变成红色,更新上来的。

分析:p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色法解决问题,需要旋转+变色。

 如果p是g的左,c是p的左(都在同一边),那么以g为旋转点进行右单旋,再把p变黑,g变红即可。p变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。

同理如果p是g的右,c是p的右(都在同一边),那么以g为旋转点进行左单旋,再把p变黑,g变红即可。p变成这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为p的父亲是黑色还是红色或者空都不违反规则。

5.2.3双旋+变色


c为红,p为红,g为黑,u不存在或者u存在且为黑,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是黑色的,是在c的子树中插入,符合情况1,变色将c从黑色变成红色,更新上来的。

分析:p必须变黑,才能解决,连续红色结点的问题,u不存在或者是黑色的,这里单纯的变色无法解决问题,需要旋转+变色。

我们来看如果不在同一边双旋的情况。

如果p是g的左,c是p的右,那么先以p为旋转点进行左单旋,再以g为旋转点进行右单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的亲父是黑色还是红色或者空都不违反规则。

同理如果p是g的右,c是p的左,那么先以p为旋转点进行右单旋,再以g为旋转点进行左单旋,再把c变黑,g变红即可。c变成课这颗树新的根,这样子树黑色结点的数量不变,没有连续的红色结点了,且不需要往上更新,因为c的父亲是黑色还是红色或者空都不违反规则。

5.3红黑树插入代码实现

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;}}//此时parent就是要插入位置的父亲节点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){//    g// p    uNode* uncle = grandfather->_right;if (uncle && uncle->_col == RED){//变色parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续向上处理cur = grandfather;parent = cur->_parent;}else{//叔叔不存在或者叔叔存在且为黑if (cur == parent->_left){//             g//     p              u// cRotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{//             g//     p              u//          cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}else{//   g//u     pNode* uncle = grandfather->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续向上处理cur = grandfather;parent = cur->_parent;}else{//叔叔不存在或者叔叔存在且为黑//     g//u       p//             cif (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;Node* pParent = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_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;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;if (parentParent == nullptr){_root = subR;subR->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subR;}else{parentParent->_right = subR;}subR->_parent = parentParent;}}

5.4查找

按⼆叉搜索树逻辑实现即可,搜索效率为 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;
}

5.5红黑树的检验

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

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

1. 规则1枚举颜色类型,天然实现保证了颜色不是黑色就是红色。

2. 规则2直接检查根即可

3. 规则3前序遍历检查,遇到红色结点查孩子不太方便,因为孩子有两个,且不一定存在,反过来检查父亲的颜色就方便多了。

4. 规则4前序遍历,遍历过程中用形参记录跟到当前结点的blackNum(黑色结点数量),前序遍历遇到黑色结点就++blackNum,走到空就计算出了⼀条路径的黑色结点数量。再任意⼀条路径黑色结点数量作为参考值,依次比较即可

 

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

六.整体代码

#pragma once
enum Colour
{RED,BLACK
};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: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;}}//此时parent就是要插入位置的父亲节点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){//    g// p    uNode* uncle = grandfather->_right;if (uncle && uncle->_col == RED){//变色parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续向上处理cur = grandfather;parent = cur->_parent;}else{//叔叔不存在或者叔叔存在且为黑if (cur == parent->_left){//             g//     p              u// cRotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{//             g//     p              u//          cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}else{//   g//u     pNode* uncle = grandfather->_left;if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续向上处理cur = grandfather;parent = cur->_parent;}else{//叔叔不存在或者叔叔存在且为黑//     g//u       p//             cif (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;Node* pParent = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_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;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;if (parentParent == nullptr){_root = subR;subR->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subR;}else{parentParent->_right = subR;}subR->_parent = parentParent;}}void InOrder(){_InOrder(_root);cout << endl;}int Height(){return _Height(_root);}int Size(){return _Size(_root);}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;}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);}
private: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);}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}int _Height(Node* root){if (root == nullptr)return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}int _Size(Node* root){if (root == nullptr)return 0;return _Size(root->_left) + _Size(root->_right) + 1;}private:Node* _root = nullptr;
};

总结: 

简单的红黑树的结构我们已经有了一些了解,下一期就让我们来看看怎么用红黑树来封装实现map和set吧

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

相关文章:

  • 精益数据分析(58/126):移情阶段的深度实践与客户访谈方法论
  • 全面解析 Server-Sent Events(SSE)协议:从大模型流式输出到实时通信场景
  • Spring MVC数据绑定和响应 你了解多少?
  • 如何下载和安装 Ghost Spectre Windows 11 24H2 PRO
  • 102. 二叉树的层序遍历递归法:深度优先搜索的巧妙应用
  • 软件设计师考试《综合知识》计算机编码考点分析
  • [Linux] vim及gcc工具
  • Spring中的循环引用
  • 一发入魂:极简解决 SwiftUI 复杂视图未能正确刷新的问题(上)
  • LabVIEW中样条插值实现及应用
  • Qwen集成clickhouse实现RAG
  • C# 调试技巧——日志记录,NuGet内断点
  • 【HCIA】BFD
  • 化工单元操作实训装置JGSX-205计算机过程控制流体输送操作实训装置
  • 环境配置与MySQL简介
  • 信息安全入门基础知识
  • Python操作MySQL 连接加入缓存层完整方案
  • 【MySQL】(11) 索引
  • 【Java学习笔记】equals方法
  • ElasticSearch重启之后shard未分配问题的解决
  • Rocky Linux 9.5 基于kubeadm部署k8s
  • 3天云南旅游规划
  • Kafka 消费者组进度监控方法解析
  • 【SSL部署与优化​】​​TLS 1.3的核心改进与性能优化​​
  • Java构造器封装继承重写多态
  • Doris
  • Kotlin 中 infix 关键字的原理和使用场景
  • A2DP、HFP、AVRCP分别是什么
  • STL,智能指针和线程安全自选锁读者写者问题
  • 蓝桥杯13届国B 完全日期