C++:用红黑树封装map与set-1
文章目录
- 前言
- 一、STL源码分析
- 二、红黑树的构建
- 三、map与set整体框架的搭建与解析
- 四、如何取出进行比较?
- 1. met与set的数据是不同的
- 2. 取出数据进行比较
- 1)问题发现
- 2)仿函数解决
- 五、封装插入
- 六、迭代器的实现
- 1. operator* 与operator->
- 2. operator!=
- 3. operator++
- 4. operator- -
- 5. 套用普通迭代器
- 七、const迭代器
- 八、查找
- 总结
前言
之前我们学习了红黑树的实现,现在我们一起来看一看如何使用红黑树封装出set与map~~~
一、STL源码分析
我们一起来分析分析STL源码,看一看库中是如何实现的🥰🥰
首先,库里面stl_set与stl_map中,
对于set
来说,key_type
是key
,value_type
也是key
,也就是说set是一个rbTree<Key, Key>
的模型。
对于map
来说,key_type
是key
,但是value_type
是pair<const key, T>
,也就是说map是一个rbTree<Key, pair<Key, Value>>
的模型。
我们再来看一下rb_tree的结构:
rb_tree中,前两个参数是<key, value>,而__rb_tree_node<value>
里面的参数传的是value,因此我们可以总结出,这里map的结点
中存储的是pair<key, value>
,而set的结点
中存储的是key
。
那么到底为什么是这样的结构呢?
我们将在下面的讲解中逐一解释🥰
二、红黑树的构建
对于红黑树的构建,J桑之前的文章有详细的讲解,因此这里就直接给出代码啦,还不清楚的观众老爷门可以点击下方的传送门🥰🥰🥰
深入探索:C++红黑树原理与实现
当然,我们之前红黑树是默认存储pair,现在要同时满足map与set,因此一些地方需要改变,之后总结的时候会给出完整的更改后的代码,下面讲解也会被提到。
#pragma once
#include<iostream>using namespace std;enum Colour
{RED,BLACK
};template<class K, class V>
struct RBTreeNode
{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){// 小于往左走if (kv.first < cur->_kv.first){parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first) // 大于往右走{parent = cur;cur = cur->_right;}else{return false;}}cur = new Node(kv);// 其他结点初始颜色为红色cur->_col = RED;// 链接if (cur->_kv.first < parent->_kv.first){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;// 如果我们parent是黑色,不用处理,就结束了// 情况一:cur为红,parent为红,grandfather为黑,uncle不确定// 用while循环是因为我们要不断的向上调整while (parent && parent->_col == RED){// 首先我们要找到grandfatherNode* grandfather = parent->_parent;// 接下来通过grandfather找到uncle//如果parent是grandfather->_leftif (parent == grandfather->_left){// 说明uncle在右边Node* uncle = grandfather->_right;// uncle存在且为红if (uncle && uncle->_col == RED){// 满足上述情况,开始调整颜色parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续向上调整cur = grandfather;parent = cur->_parent;// 但是走到这里有一个问题,如果当前parent就是根,// 并且parent是红色,我们要把它变为黑色,// 处理方式是:出循环后,暴力将_root->_col变为黑色}else // uncle不存在 或 uncle存在且为黑色{// 判断要怎样旋转// 右单旋if (cur == parent->_left){// g// p// cRotateR(grandfather);// 调整颜色parent->_col = BLACK;grandfather->_col = RED;}else // 左右双旋{// g// p// cRotateL(parent);RotateR(grandfather);// 调整颜色cur->_col = BLACK;grandfather->_col = RED;}// 旋转变色完就结束了,这里不加这个也可以,条件判断就会退出break;}}else //(parent == grandfather->_right) // 如果parent是grandfather->_right{// 说明uncle在左边Node* uncle = grandfather->_left;// uncle存在且为红if (uncle && uncle->_col == RED){// 满足上述情况,开始调整颜色parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续向上调整cur = grandfather;parent = cur->_parent;}else // uncle不存在 或 uncle存在且为黑色{// 判断要怎样旋转// 左单旋if (cur == parent->_right){// g// p// cRotateL(grandfather);// 调整颜色parent->_col = BLACK;grandfather->_col = RED;}else // 右左双旋{// g// p// cRotateR(parent);RotateL(grandfather);// 调整颜色cur->_col = BLACK;grandfather->_col = RED;}break;}}}// 这里保证根为黑色_root->_col = BLACK;return true;}// 左单旋void RotateL(Node* parent){Node* cur = parent->_right;Node* curleft = cur->_left;// 重新链接parent->_right = curleft;if (curleft) // 如果curleft存在{curleft->_parent = parent;}cur->_left = parent;Node* ppnode = parent->_parent;parent->_parent = cur;if (ppnode == nullptr){_root = cur;cur->_parent = nullptr;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}}// 右单旋void RotateR(Node* parent){Node* cur = parent->_left;Node* curright = cur->_right;parent->_left = curright;if (curright){curright->_parent = parent;}cur->_right = parent;Node* ppnode = parent->_parent;parent->_parent = cur;if (ppnode == nullptr){cur->_parent = nullptr;_root = cur;}else{if (ppnode->_left == parent){ppnode->_left = cur;}else{ppnode->_right = cur;}cur->_parent = ppnode;}}// 检查是否构建正确bool CheckColour(Node* root, int blacknum, int benchmark){if (root == nullptr){if (blacknum != benchmark)return false;return true;}if (root->_col == BLACK){++blacknum;}if (root->_col == RED && root->_parent && root->_parent->_col == RED){cout << root->_kv.first << "出现连续红色节点" << endl;return false;}return CheckColour(root->_left, blacknum, benchmark)&& CheckColour(root->_right, blacknum, benchmark);}bool IsBalance(){return IsBalance(_root);}bool IsBalance(Node* root){if (root == nullptr)return true;if (root->_col != BLACK){return false;}// 基准值int benchmark = 0;Node* cur = _root;while (cur){if (cur->_col == BLACK)++benchmark;cur = cur->_left;}return CheckColour(root, 0, benchmark);}private:Node* _root = nullptr;
};
三、map与set整体框架的搭建与解析
代码:
namespace jyf
{template<class k, class v>class map{public:private:RBTree<k, pair<k, v>> _t;};
}
namespace jyf
{template<class k>class set{public:private:RBTree<k, k> _t;};
}
解析:
首先无论是map还是set都有两个模板参数,第一个是key,这个key是多存的,它的作用体现在像find()这样的函数中,我们后面在做解析。
这里先看第二个参数,传给了RBTree
的T
,而T就是结点中储存的东西,也就是说,你是set
,那么节点中就储存的是key
, 你是map
,那么节点中就储存的是pair<k, v>
。
四、如何取出进行比较?
1. met与set的数据是不同的
我们要明白一个我问题,met与set结点中存储的数据是不一样的,因此,如果节点中还存储的是pair就不对了,因此,我们结点之中存储的数据因该是T类型的数据,如果是set就是key,如果是map就是pair。
2. 取出数据进行比较
1)问题发现
再insert函数中,我们之前的红黑树是这样实现的,比如这个找到插入位置的逻辑:
在这张图片里面,我们之前使用pair,但是现在对于map和set存储的数据不同,因此需要用data来比较。
但是!!!
对于map来说,它的value是pair,但是pair的比较逻辑能满足我们的需要吗?
可以看到,pair的比较逻辑是先比first,first一样就比second,但是,我们这里不需要比较second,key_value的模型中,只需要比较key不同,因此我们需要一种方法,重新定义我们的比较。
怎么重新比较呢?
这里我们通过观察发现,对于set来说,它的data是key,可以直接比较,唯一有问题的是map,因此我们采取的方式是仿函数。
2)仿函数解决
我们可以多定义一个模板参数KeyOfT
,这个模板参数用来定义仿函数,他的作用是取出Set或Map中的Key。
对于Set,它的data直接就是key:
struct SetKeyOfT
{const K& operator()(const K& key){return key;}
};
对于Map,它的data是一个pair,我们需要pair的first,也就是key:
struct MapKeyOfT
{const K& operator()(const pair<K,V>& kv){return kv.first;}
};
至此,我们在取出数据的时候,只要定义出一个对象,重载operator()
,用()
将data包起来,就得到了我们想要的数据。
可以说,这里set迁就了map~
五、封装插入
完成了上述步骤,我们就可以实现封装map与set的插入了~
对于set:
bool insert(const K& key)
{return _t.Insert(key);
}
对于map:
bool insert(const pair<K,V>& kv)
{return _t.Insert(kv);
}
插入结果:
六、迭代器的实现
要实现迭代器,就要先理解迭代器是怎么用的:
下面是一个模板表示迭代器的使用:
it = s.begin();
while (it != s.end())
{cout << *it << endl;++it;
}
为了实现这个过程,我们需要重载很多东西。
我们先将框架搭出来:
template<class T>
struct __TreeIterator
{typedef RBTreeNode<T> Node;typedef __TreeIterator<T> Self;Node* _node;__TreeIterator(Node* node):_node(node){}
};
1. operator* 与operator->
RBTree中:
T& operator* ()
{return _node->_data;
}T* operator-> ()
{return &(_node->_data);
}
2. operator!=
bool operator!= (const Self& s)
{return _node != s._node;
}
3. operator++
对于树的迭代器,++与- -就非常重要了,这里有很多坑~
首先对于一个红黑树,他走的是中序的排序,图如下:
那么,it.begin()
是谁呢?
我们说中序是左-根-右
,也就是说,begin应该是上图中的1
。
其次,如果我们进行++操作,迭代器会到那里去呢?
这里分以下几种情况:
- 如果右不为空
那么下一个访问的,将是右树的最左:
- 如果右为空
这里又分两种情况:
-
cur是parent的左——下一个访问parent
-
cur是parent的右——下一个访问没有被访问的祖先
通过观察这两种情况我们可以发现:
也就是说,当右为空的时候,下一个访问的是孩子是父亲的左侧的那一个祖先。
代码总结:
Self& operator++ ()
{if (_node->_right != nullptr){Node* curleft = _node->_right;while (curleft->_left){curleft = curleft->_left;}_node = curleft;}else{// 找孩子是父亲左的那个祖先节点,就是下一个要访问的节点Node* cur = _node;Node* parent = _node->_parent;while (parent){if (parent->_left == cur){break;}else{cur = parent;parent = parent->_parent;}}_node = parent;}return *this;
}
4. operator- -
原理与++是一样的,只不过原本++的顺序是中序,即左-根-右
,- - 是反过来的,因此是右-根-左
Self& operator--(){if (_node->_left){Node* subRight = _node->_left;while (subRight->_right){subRight = subRight->_right;}_node = subRight;}else{// 孩子是父亲的右的那个节点Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = cur->_parent;parent = parent->_parent;}_node = parent;}return *this;}
5. 套用普通迭代器
RBTree中:
经过前面的分析,begin就是树最左边的结点,end我们设置为nullptr。
typedef __TreeIterator<T> iterator;
public:iterator begin(){Node* leftMin = _root;while (leftMin && leftMin->_left){leftMin = leftMin->_left;}return iterator(leftMin);}iterator end(){return iterator(nullptr);}
set与map中,我们要封装这个方法:
iterator begin(){return _t.begin();}iterator end(){return _t.end();}
测试普通迭代器:
jyf::map<int, int> m;
m.insert(make_pair(1, 1));
m.insert(make_pair(3, 3));
m.insert(make_pair(2, 2));jyf::map<int, int>::iterator mit = m.begin();while (mit != m.end())
{mit->first = 1;mit->second = 2;cout << mit->first << ":" << mit->second << endl;++mit;
}
cout << endl;for (const auto& kv : m)
{cout << kv.first << ":" << kv.second << endl;
}
cout << endl;jyf::set<int> s;
s.insert(5);
s.insert(2);
s.insert(2);
s.insert(12);
s.insert(22);
s.insert(332);
s.insert(7);auto it = s.begin();
while (it != s.end())
{// Ӧif (*it % 2 == 0){*it += 10;}cout << *it << " ";++it;
}
cout << endl;for (const auto& e : s)
{cout << e << " ";
}
cout << endl;
结果为:
七、const迭代器
我们知道set是不允许修改的,map的key不允许修改,而value允许修改,再通过观察库中的实现,我们可以发现:
set实现不能修改的原因是——interator迭代器与const_iterator都是const迭代器。
而map实现key不能修改,value可以修改的方法是,在定义map的value的时候,pair<K, V>
修改为pair<const K, V>
具体的逻辑我们下一次在进行讲解~
八、查找
查找是通过key来查找的,而不是通过value来查找的,这也就解释了为什么最开始定义模板参数还要多定义一个key。
同样,为了取出对应的值,我们也需要仿函数来包上data。
Node* Find(const K& key)
{Node* cur = _root;KeyOfT kot;while (cur){if (kot(cur->_data) < key){cur = cur->_right;}else if (kot(cur->_data) > key){cur = cur->_left;}else{return cur;}}return nullptr;
}
总结
红黑树在 STL 的应用
set 与 map 的实现:
set 节点存储 key(rbTree<Key, Key> 模型)。
map 节点存储 pair<const Key, T>(rbTree<Key, pair<Key, Value>> 模型)。
rbTree 的设计:
节点使用 __rb_tree_node,value 的具体含义根据容器类型不同而不同。
相关文章:

C++:用红黑树封装map与set-1
文章目录 前言一、STL源码分析二、红黑树的构建三、map与set整体框架的搭建与解析四、如何取出进行比较?1. met与set的数据是不同的2. 取出数据进行比较1)问题发现2)仿函数解决 五、封装插入六、迭代器的实现1. operator* 与operator->2. …...
HBU算法设计与分析 贪心算法
1.最优会场调度 #include <bits/stdc.h> using namespace std; const int N1e55; typedef pair<int,int> PII; PII p[N]; priority_queue<int,vector<int>,greater<int>> q; //最小堆 存储最早结束的会场的结束时间 int n; //其实这个题可以理…...

python pycharm安装教程及基本使用,超详细
一.PyCharm下载及安装 1.1 进入pycharm官网,点击下载,下载社区版本(日常学习使用够用了),专业版是收费的哦(功能更强大) Download PyCharm: The Python IDE for data science and web development by Jet…...
变量提升函数提升
示例 1:变量提升 原始代码: console.log(x); // 输出: undefined var x 5; console.log(x); // 输出: 5提升后的代码(理解为): var x; // 变量声明被提升 console.log(x); // 输出: undefined x 5; // 赋值 conso…...

el-table vue3统计计算数字
固定合计在最下列 父组件 <template><el-tablev-loading"loading"tooltip-effect"light":data"list"style"width: 100%":max-height"maxHeight"element-loading-text"拼命加载中...":header-cell-styl…...

IDE应当具备的功能
IDE 是辅助编程的工具,应当具备以下功能 语法高亮 显示注释 显示光键词 显示括号 matlab 自带的 IDE 没有这个功能 显示缩进 matlab 自带的 IDE 没有这个功能 显示字符串 显示数字常量 定位到函数的定义位置 Matlab 自带的集成开发环境(IDE&am…...
Stable Diffusion初步见解(二)
Stable Diffusion 是一种先进的深度学习模型,用于生成高质量的图像和艺术作品。它基于扩散模型(Diffusion Models),并结合了潜在扩散模型(Latent Diffusion Models)以及条件生成技术(如文本到图…...

前端框架 react 性能优化
目录 一、不使用任何性能优化API进行优化 二、通过性能优化API优化 1、React.memo 2、useCallback 3、useMemo 4、PureComponent 三、总结 总览:react的优化核心思想就是让react跳过重新渲染那个些没有改变的Component,而只重新渲染发生变化的C…...

RK3568平台开发系列讲解(Input子系统篇)输入子系统介绍
🚀返回专栏总目录 文章目录 一、什么是输入子系统?二、输入设备和节点的关系沉淀、分享、成长,让自己和他人都能有所收获!😄 一、什么是输入子系统? 在 Linux 中,input 子系统是专门为处理输入类设备而设计的一个子系统或框架。它提供 了一套通用的接口和机制,用于驱…...

准备阶段 Profiler性能分析工具的使用(一)
Unity 性能分析器 (Unity Profiler) 性能分析器记录应用程序性能的多个方面并显示相关信息。使用此信息可以做出有关应用程序中可能需要优化的事项的明智决策,并确认所做的优化是否产生预期结果。 默认情况下,性能分析器记录并保留游戏的最后 300 帧&a…...
go-rod vs Selenium:自动化测试工具的比较与选择
自动化测试是软件开发过程中的关键环节,它能够帮助我们发现缺陷、验证功能并提高软件质量。随着Web技术的快速发展,市场上出现了多种自动化测试工具,其中Selenium和go-rod是两个备受关注的选择。本文将从多个维度对这两个工具进行比较&#x…...

探索免费的Figma中文版:开启高效设计之旅
在当今数字化设计的浪潮中,Figma以其强大的云端协作功能和出色的设计能力,成为了众多设计师的心头好。而对于国内的设计师来说,能够免费使用Figma中文版更是一大福音,下面就来一起探索一下吧。 一、Figma中文版的获取途径 虽然F…...

功能齐全,支持协作 | Docker部署一款支持多人共享的私密浏览器『n.eko』
功能齐全,支持协作 | Docker部署一款支持多人共享的私密浏览器『n.eko』 哈喽小伙伴们好,我是Stark-C~ 玩NAS的朋友基本都会在本地部署一款浏览器用来远程访问内网的网络设备,或者偶尔拿来浏览一些私密网站都是很方便的。 今天为大家分享的…...

部署实战(二)--修改jar中的文件并重新打包成jar文件
一.jar文件 JAR 文件就是 Java Archive ( Java 档案文件),它是 Java 的一种文档格式JAR 文件与 ZIP 文件唯一的区别就是在 JAR 文件的内容中,多出了一个META-INF/MANIFEST.MF 文件META-INF/MANIFEST.MF 文件在生成 JAR 文件的时候…...
Ubuntu24.04——软件包系统已损坏
如果你在使用 Ubuntu 时遇到“软件包系统已损坏”的问题,可以尝试以下步骤来修复它: 更新软件包列表: 打开终端,运行以下命令以更新软件包列表: sudo apt update修复损坏的软件包: 运行以下命令来修复损坏的…...
2024年华为OD机试真题-空栈压数-C++-OD统一考试(E卷)
最新华为OD机试考点合集:华为OD机试2024年真题题库(E卷+D卷+C卷)_华为od机试题库-CSDN博客 每一题都含有详细的解题思路和代码注释,精编c++、JAVA、Python三种语言解法。帮助每一位考生轻松、高效刷题。订阅后永久可看,发现新题及时跟新。 题目描述: 向一个空栈压入…...

嵌入式Linux基于IMX6ULL tslib学习总结
目录 1. tslib开源库介绍1.1 tslib主要功能1.2 架构 2. tslib代码简单分析2.1 ts_print_mt.c分析代码2.2 ts_setup代码分析2.3 ts_open代码分析2.4 ts_config代码分析2.5 ts_read_mt代码分析2.6 tslib中4个模块的含义 3. 使用tslib库打印触摸屏2点之间的距离 基于韦东山IMX6ULL…...
go中的参数传递是值传递还是引用传递?
在Go语言中,参数传递机制是一个重要的概念,它决定了函数内部对参数的修改是否会影响到原始数据。关于Go中的参数传递是值传递还是引用传递的问题,可以从以下几个方面进行解答。 一、值传递与引用传递的定义 值传递:在值传递中&a…...

记录一种在内核空间向用户空间通知中断的方法
记录一种在内核空间向用户空间通知中断的方法 0.前言1.代码实现1)内核设备驱动实现2)消息通知实现3)测试程序 2.解析 参考文章:Linux驱动实践:中断处理函数如何【发送信号】给应用层? 0.前言 最近在项目中遇到一个需求,需要将一个…...
.NetCore 过滤器和拦截器 的区别
Asp.NET Core 中的过滤器(Filter)和拦截器(Interceptor)是两个不同的概念,但它们在某些方面有相似之处,也有明显的区别。 🔑过滤器(Filter) 过滤器是Asp.NET Core中用于…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:
一、属性动画概述NETX 作用:实现组件通用属性的渐变过渡效果,提升用户体验。支持属性:width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项: 布局类属性(如宽高)变化时&#…...

UDP(Echoserver)
网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法:netstat [选项] 功能:查看网络状态 常用选项: n 拒绝显示别名&#…...

Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...

GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
Linux安全加固:从攻防视角构建系统免疫
Linux安全加固:从攻防视角构建系统免疫 构建坚不可摧的数字堡垒 引言:攻防对抗的新纪元 在日益复杂的网络威胁环境中,Linux系统安全已从被动防御转向主动免疫。2023年全球网络安全报告显示,高级持续性威胁(APT)攻击同比增长65%,平均入侵停留时间缩短至48小时。本章将从…...
13.10 LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析
LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析 LanguageMentor 对话式训练系统架构与实现 关键词:多轮对话系统设计、场景化提示工程、情感识别优化、LangGraph 状态管理、Ollama 私有化部署 1. 对话训练系统技术架构 采用四层架构实现高扩展性的对话训练…...