【C++】封装map和set(红黑树实现)

前言:

       前面,我们学习了set和map的用法,这两个容器可以完成查找,排序等操作,后来我们在学习过二叉搜索树的基础上又学习了两种特殊的二叉搜索树——AVL树和红黑树,他们俩可以是效率进一步提高,其实set和map的底层就是由红黑树封装而成的!

       所以,本篇文章我们自己学习用红黑树封装set和map。用红黑树封装,特别是用同一棵红黑树实现封装有一定的难度,其中很多操作(比如复用)我们会第一次尝试,所以大家请跟紧作者的脚步来。

目录

(一)如何复用同一棵红黑树

(二)红黑树的改造流程(为封装做铺垫)

1、结点的定义

2、模拟实现结点的比较大小

3、改造后的红黑树

4、map和set迭代器的模拟实现

4.1模版参数的构成

4.2begin()和end()的模拟实现

4.3operator* 和 operator->模拟实现

4.4 operator++ 和 operator--模拟实现

4.5 operator== 和 operator!=模拟实现

4.6迭代器代码详解

 (三)封装map和set


(一)如何复用同一棵红黑树

前提疑问:

在我们这所有的之前我们知道,map和set这两个容器都是用红黑树来实现的,那么就有了接下来的问题。

  • map和set都是用的同一棵红黑树复用的吗
  • 或者这两个容器各自使用一棵红黑树吗

其实在前言中我们就知道了,根据STL的设计理念和泛型编程的理念,我们不会冗余的写两课红黑树,而是用一棵红黑树实现复用的效果。

我们调用STL库中的原码来看:

我们发现:

  • STL库中的原码也确实调用的同一棵红黑树
  • 他们实现的都是key_value模型

我们设set中存放结点的值是K,map中存放的节点是pair<key,value>键值对:

  • 对于set而言,底层红黑树的模版就是RBTree<K, K>
  • 对于map而言,底层红黑树的模版就是RBTree<K, pair<K, V>>

这时我们就有了另一个疑问,两个模板参数的第一个Key,不能省略掉吗??

  • 首先,答案肯定是不能的
  • 那么原因又是什么呢?

因为map的这个类中,无论怎么省略都会有一个查找函数要单独用到Key

如果第一个Key删去了,那么map和set的查找函数就没法统一实现了,违背了我们一开始泛型编程的思想。

综上所述:

  • map和set都是用了,Key_value模型
  • set中的K是K,V也是K
  • map中的K是K,V是pair<K,V>
  • 并且模板参数中第一个K都不能省

(二)红黑树的改造流程(为封装做铺垫)

1、结点的定义

这里由于set和map存放的结点一个是Key一个是pair<Key,Value>,所以我们使用模版,把存放的结点泛化。

2、模拟实现结点的比较大小

  • 上述提到,在模拟实现中,map和set我们复用同一棵红黑树的时候都是用的是Kye_value的结构
  • 但是红黑树中的数据比较又是Key值的比较,而现在我们用的则是pair的比较
  • 虽然编译上是可以通过但是真的就是我们所想要的吗?

pair的比较大小:

很显然这种比较规则不是我们所想要的,并且map和set想要取到用来比较的数据是不同的。 

为了取到我们想要的数据,我们引入了仿函数:

  • 根据map和set的需求不同
  • 我们在红黑树中新引入了一个模板参数KeyOfT

 

引入KeyOfT模板参数后,我们想要不同方式的比较方法只需要在set和map的封装中给出属于他们自己的比较仿函数。

map中的仿函数: 

set中的仿函数:

使用方法:

 此时前面所说的仿函数的便利之处就体现出来了。

3、改造后的红黑树

具体代码:

//红黑树的实现
//KeyOfT --> 支持取出T对象中key的仿函数
template<class K, class T, class KeyOfT>
class RBTree
{typedef RBTreeNode<T> Node;
public:typedef __RBTreeIterator<T, T&, T*> iterator;typedef __RBTreeIterator<T, const T&, const T*> const_iterator;//构造 拷贝构造 赋值 和析构 跟搜索树实现方式是一样的//迭代器中序遍历,要找最左结点iterator Begin(){Node* subLeft = _root;while (subLeft && subLeft->_left){subLeft = subLeft->_left;}//树的迭代器用结点的指针就可以构造return iterator(subLeft);}iterator End(){return iterator(nullptr);}const_iterator Begin() const{Node* subLeft = _root;while (subLeft && subLeft->_left){subLeft = subLeft->_left;}//树的迭代器用结点的指针就可以构造return const_iterator(subLeft);}const_iterator End() const{return const_iterator(nullptr);}pair<iterator, bool> Insert(const T& data){//1、搜索树的规则插入//2、看是否违反平衡规则,如果违反就需要处理:旋转if (_root == nullptr){_root = new Node(data);_root->_col = BLACK; //根节点是黑色return make_pair(iterator(_root), true);}KeyOfT kot;Node* parent = nullptr;Node* cur = _root;while (cur){if (kot(cur->_data) < kot(data)){parent = cur;cur = cur->_right;}else if (kot(cur->_data) > kot(data)){parent = cur;cur = cur->_left;}else{return make_pair(iterator(cur), false);}}//找到符合规则的位置之后再插入cur = new Node(data); Node* newnode = cur;cur->_col = RED;if (kot(parent->_data) < kot(data)){parent->_right = cur;}else{parent->_left = cur;}//三叉链的链接 -- 链上父节点cur->_parent = parent;//存在连续红色结点while (parent && parent->_col == RED){//理论而言,祖父是一定存在的,父亲存在且是红不可能是根(根一定是黑的)Node* grandfather = parent->_parent;assert(grandfather);if (grandfather->_left == parent){Node* uncle = grandfather->_right;//情况一:(叔叔存在且为红)if (uncle && uncle->_col == RED){//祖父和叔叔变成黑色parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}//情况二:(叔叔不存在 or 叔叔存在且为黑)else{//单旋//	   g//	 p// cif (cur == parent->_left){RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}//双旋//    g//  p//    celse{RotateL(parent);RotateR(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}//无论父亲和叔叔是左是右都是一样的//grandfather->_right == parent;else{Node* uncle = grandfather->_left;//情况一:if (uncle && uncle->_col == RED){//祖父和叔叔变成黑色parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}else{//单旋// g//   p//     cif (cur == parent->_right){RotateL(grandfather);parent->_col = BLACK;grandfather->_col = RED;}//双旋//  g//    p//  celse{RotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}//父亲为空就出循环,将根节点设置成黑色_root->_col = BLACK;return make_pair(iterator(newnode), true);}
}

插入(Insert)和寻找(Find)代码:
 

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;}pair<itertaor, bool> Insert(const T& data){if (_root == nullptr){_root = new Node(data);_root->_col = BLACK;return make_pair(itertaor(_root), true);}KeyOfT kot;Node* parent = nullptr;Node* cur = _root;while (cur){if (kot(cur->_data) < kot(data)){parent = cur;cur = cur->_right;}else if (kot(cur->_data) > kot(data)){parent = cur;cur = cur->_left;}else{return make_pair(itertaor(cur), false);}}cur = new Node(data);Node* newnode = cur;if (kot(parent->_data) > kot(data)){parent->_left = cur;}else{parent->_right = cur;}cur->_parent = parent;while (parent && parent->_col == RED){Node* grandfather = parent->_parent;if (grandfather->_left == parent){Node* uncle = grandfather->_right;// 情况1:u存在且为红,变色处理,并继续往上处理if (uncle && uncle->_col == RED){parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续往上调整cur = grandfather;parent = cur->_parent;}else // 情况2+3:u不存在/u存在且为黑,旋转+变色{//     g//   p   u// c if (cur == parent->_left){RotateR(grandfather);parent->_col = BLACK;grandfather->_col = RED;}else{//     g//   p   u//     cRotateL(parent);RotateR(grandfather);cur->_col = BLACK;//parent->_col = RED;grandfather->_col = RED;}break;}}else // (grandfather->_right == parent){//    g//  u   p//        cNode* uncle = grandfather->_left;// 情况1:u存在且为红,变色处理,并继续往上处理if (uncle && uncle->_col == RED){parent->_col = BLACK;uncle->_col = BLACK;grandfather->_col = RED;// 继续往上调整cur = grandfather;parent = cur->_parent;}else // 情况2+3:u不存在/u存在且为黑,旋转+变色{//    g//  u   p//        cif (cur == parent->_right){RotateL(grandfather);grandfather->_col = RED;parent->_col = BLACK;}else{//    g//  u   p//    cRotateR(parent);RotateL(grandfather);cur->_col = BLACK;grandfather->_col = RED;}break;}}}_root->_col = BLACK;return make_pair(itertaor(newnode), true);;}

4、map和set迭代器的模拟实现

上面改造后的红黑树中已经涉及到了迭代器,这里我们来模拟实现。

备注:

  • T:数据
  • Ref:引用
  • Ptr:指针
  • Sef:iterator本身

4.1模版参数的构成

要想实现同一棵红黑树的复用,模版参数的构成极为重要。

之前我们也遇到过相似的情况,我们这里的实现方法参看之前。

迭代器的实现中:

  • 涉及到了*解引用操作,返回的是数据的引用;
  • 涉及到了->指针操作,返回的是数据的地址;
  • 涉及到++,--操作,返回的是操作后迭代器本身的位置。

但是以上的行为,我们都涉及结点,所以结点的数据类型也尤为重要

综上,模版参数的构成如下图:

4.2begin()和end()的模拟实现

  • 因为是二叉搜索树,为了更有顺序,所以我们采取的是中序遍历。
  • 那么中序遍历Begin()就应该是最左结点
  • 我们实现的版本中End()定义为空是(nullptr)

ps:

而库中的红黑树则是设计了一个哨兵位的头结点:

所以我们实现的只是简化版本。

4.3operator* 和 operator->模拟实现

就是正常的运用operator,但是operator->和list中讲的一样,返回的是地址的原因是为了连续访问(连续的operator->会优化成一个->)

4.4 operator++ 和 operator--模拟实现

这里迭代器的++和- - 需要分类一下,分别是:前置++,- - 、后置++,- -

前置++ :
因为我们是中序遍历,我们访问完自己之后,下一个该访问哪一个结点?

我们观察任何一棵二叉树都能看出,无非是下面两种情况:

分如下两种情况:(重点)

  • 右子树为空:找孩子是父亲的左的那个祖先节点,否则继续往上走,直到空(nullptr)
  • 右子树为非空:找右子树的最左节点

这一过程有点类似于递归思想,但是是非递归实现的

代码实现:

 前置-- :

有了上面的思路,我们联想一下,现在需要看当前位置左子树是否是空。

前置- -就是倒着走,同样还是对当前位置分两种情况:(重点)

  1. 左子树为空:找孩子是父亲的右的那个祖先节点,否则继续往上走,直到空(nullptr)
  2. 左子树为非空:找左子树的最右节点

只要前置++理解了,那么前置- -完全就是前置++倒过来走一遍。

后置++、后置- - :

和之前实现容器的得带器一样,我们这里直接复用即可:

4.5 operator== 和 operator!=模拟实现

有了之前封装迭代器的经验,这两个的视线还是比较容易得:

4.6迭代器代码详解

template<class T, class Ref, class Ptr>
struct __RBTreeIterator
{typedef RBTreeNode<T> Node;Node* _node;typedef __RBTreeIterator<T, Ref, Ptr> Self;__RBTreeIterator(Node* node):_node(node){}Ref operator*(){return _node->_data;}Ptr operator->(){return &_node->_data;}Self& operator++(){if (_node->_right == nullptr){//找祖先里面,孩子是父亲左的那个Node* cur = _node;Node* parent = cur->_parent;while (parent && parent->_right == cur){cur = cur->_parent;parent = parent->_parent;}_node = parent;}else{//右子树的最左结点Node* subLeft = _node->_right;while (subLeft->_left){subLeft = subLeft->_left;}//左为空_node = subLeft;}return *this;}Self& operator--(){if (_node->_left == nullptr){//找祖先里面,孩子是父亲Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = cur->_parent;parent = parent->_parent;}_node = parent;}else{//左子树的最右结点Node* subRight = _node->_left;while (subRight->_right){subRight = subRight->_right;}_node = subRight;}return *this;}Self operator++(int){Self tmp(*this);++(*this);return tmp;}Self operator--(int){Self tmp(*this);--(*this);return tmp;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}
};

 (三)封装map和set

有了上面的红黑树的改装,我们这里的对map和set的封装就显得很得心应手了。

封装主要是把map和set的基本操作封装在一个类中。

map的封装:

template<class K, class V>
class map
{//定义一个内部类struct MapKeyOfT{const K& operator()(const pair<K, V>& kv)//operator() 可以像函数一样去使用{return kv.first;}};public:typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::const_iterator const_iterator;iterator begin(){return _t.Begin();}iterator end(){return _t.End();}pair<iterator, bool> insert(const pair<K, V>& kv){return _t.Insert(kv);}V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;}
private:RBTree<K, pair<K, V>, MapKeyOfT> _t;
};

这里map中的operator[ ]我们知道其原理之后,模拟实现就非常方便,直接调用插入函数,控制好参数和返回值即可。

对set的封装:

template<class K>
class set    
{struct SetKeyOfT{const K& operator()(const K& key)//operator() 可以像函数一样去使用{return key;}};
public://加上typename告诉编译器这是个类型,类模板实例化了再去取typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;iterator begin() const{return _t.Begin();}iterator end() const{return _t.End();}pair<iterator, bool> insert(const K& key){//pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> ret = _t.Insert(key);auto ret = _t.Insert(key);return pair<iterator, bool>(iterator(ret.first._node), ret.second);}iterator find(const K& key){return _t.Find(key);}private:RBTree<K, K, SetKeyOfT> _t;
};

附详细代码:——》红黑树封装map和set

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

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

相关文章

Spring Security OAuth2 远程命令执行漏洞

文章目录 一、搭建环境二、漏洞验证三、准备payload四、执行payload五、变形payload 一、搭建环境 cd vulhub/spring/CVE-2016-4977/ docker-compose up -d 二、漏洞验证 访问 http://192.168.10.171:8080/oauth/authorize?response_type${233*233}&client_idacme&s…

【安全】正则回溯绕过练习简单案例

目录 环境 案例1 前要 代码审计 分析 案例2 代码审计 分析 payload 环境 phpstudy 案例1 前要 php中0 1 -1 true false null 空字符 数组之间的比较 代码审计 <?php function areyouok($greeting){return preg_match(/Merry.*Christmas/is,$greeting); //2.传…

YAML配置文件

YAML配置文件 SpringBoot中application.properties文件存在的问题&#xff1a;配置太多后难阅读和修改&#xff0c;层级结构辨识度不高。 简介 YAML是"YAML Ain’t a Markup Language"&#xff08;YAML不是一种标记语言&#xff09;的递归缩写。在开发的这种语言时&a…

前端(十六)——Web应用的安全性研究

&#x1f642;博主&#xff1a;小猫娃来啦 &#x1f642;文章核心&#xff1a;Web应用的安全性研究 文章目录 概述常见前端安全漏洞XSS&#xff08;跨站脚本攻击&#xff09;CSRF&#xff08;跨站请求伪造&#xff09; 点击劫持安全性验证与授权用户身份验证授权与权限管理 安全…

matlab数据处理: cell table array+datetime

原数据文件.csv matlab xlsread(filename{i},B2:T2881) 会同于Excel最多1048576行 舍弃 a{1,i} xlsread(filename{i},‘B2:T2881’);%读取excel文件,选定区域’B2:G2881’ readcell(filename{i},Range,E2:M2881) 会全部读取 优选 对于日期 yyyy-MM-dd HH:mm:ss.000 matlab cel…

Spring系列文章:面向切面编程AOP

一、代理模式 1、代理模式使用场景引入 ⽣活场景1&#xff1a;⽜村的⽜⼆看上了隔壁村⼩花&#xff0c;⽜⼆不好意思直接找⼩花&#xff0c;于是⽜⼆找来了媒婆王妈妈。这 ⾥⾯就有⼀个⾮常典型的代理模式。⽜⼆不能和⼩花直接对接&#xff0c;只能找⼀个中间⼈。其中王妈妈是…

百度输入法全面升级,打造首个基于大模型的输入法原生应用

基于文心一言&#xff0c;百度输入法宣布全面升级&#xff0c;打造行业首个“基于大模型的输入法原生应用”&#xff0c;从“输入工具”全面转型为“AI创作工具”。 近日&#xff0c;百度文心一言正式向公众开放。基于文心一言&#xff0c;百度输入法宣布全面升级&#xff0c;打…

【JAVA】抽象类与接口

作者主页&#xff1a;paper jie_的博客 本文作者&#xff1a;大家好&#xff0c;我是paper jie&#xff0c;感谢你阅读本文&#xff0c;欢迎一建三连哦。 本文录入于《JAVASE语法系列》专栏&#xff0c;本专栏是针对于大学生&#xff0c;编程小白精心打造的。笔者用重金(时间和…

【Linux】——网络基础:http协议

目录 前言 应用层 认识协议 协议的概念 传输结构化数据 序列化和反序列化 网络版本计算器 服务器端Server 客户端Client 协议定制 其它 运行效果 HTTP协议 HTTP的简介 认识URL urlencode和urldecode HTTP协议格式 HTTP请求 HTTP响应 HTTP的方法 GET和POST…

顺序表详解

&#x1f493; 博客主页&#xff1a;江池俊的博客⏩ 收录专栏&#xff1a;数据结构探索&#x1f449;专栏推荐&#xff1a;✅C语言初阶之路 ✅C语言进阶之路&#x1f4bb;代码仓库&#xff1a;江池俊的代码仓库&#x1f525;编译环境&#xff1a;Visual Studio 2022&#x1f38…

Tampermonkey实践:安装引导及开发一个网页背景色更改插件

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师…

leetcode 2. 两数相加(java)

两数相加 题目描述哨兵技巧代码演示&#xff1a; 递归算法专题 题目描述 难度 - 中等 leetcode 2. 两数相加 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&…

宇凡微发布2.4G合封芯片YE08,融合高性能MCU与射频收发功能

宇凡微在2023年推出了全新的2.4G合封芯片YE08&#xff0c;该芯片结合了32位高性能MCU和强大的2.4GHz无线通信功能&#xff0c;为各种远程遥控应用提供卓越性能和广泛应用潜力。 深入了解YE08内部构造 YE08芯片内部融合了两颗强大的芯片&#xff1a;PY32F002B MCU和G350 2.4G通…

【LeetCode-中等题】78. 子集

文章目录 组合并集问题汇总&#xff1a;题目方法一&#xff1a;动态规划方法二&#xff1a;递归加回溯(关键----startIndex) 组合并集问题汇总&#xff1a; 1、子集去重版本 2、组合非去重版本 3、组合去重版本 题目 注意&#xff1a;这里的nums数组里面的元素是各不相同的&a…

OLED透明屏触控:引领未来科技革命的创新力量

OLED透明屏触控技术作为一项颠覆性的创新&#xff0c;正在引领新一轮科技革命。它将OLED显示技术与触摸技术相结合&#xff0c;实现了透明度和触控功能的完美融合。 在这篇文章中&#xff0c;尼伽将通过引用最新的市场数据、报告和行业动态&#xff0c;详细介绍OLED透明屏触控…

《python趣味工具》——酷炫二维码(3)计算机二级考试题

昨天我们学习了如何批量制作合适的二维码&#xff0c;今天来刷几道题练练手&#xff01; 文章目录 1. 制作名单2. 年会抽奖来啦3. 精准查找 1. 制作名单 秋招来了&#xff01;hr部门需要获得简历初筛后的候选者名单&#xff0c;所有候选者简历都按照“小明_xx大学.pdf”命名放…

建站系列(五)--- 前端开发语言之HTML、CSS、JavaScript

目录 相关系列文章前言一、前端开发与后端开发二、前端语言简介&#xff08;一&#xff09;、HTML&#xff08;二&#xff09;、CSS&#xff08;三&#xff09;、JavaScript 三、学习指导&#xff08;一&#xff09;、开发环境&#xff08;二&#xff09;、第一个Hello&#xf…

咖啡店小程序:吸引顾客的创新营销手段

近日&#xff0c;“酱香拿铁”的大火让大家再次把目标聚焦在年轻人都喜欢的咖啡上。现在咖啡已经成为年轻一代的社交硬通货&#xff0c;咖啡店也遍地开花。而随着移动互联网的快速发展&#xff0c;咖啡店小程序已经成为了各大咖啡店主的选择&#xff0c;因为它提供了便捷的方式…

BUUCTF rip 1

使用linux的file命令查看基本信息 64位 使用IDA64位进行反编译 看到gets就肯定有栈溢出 能看到有一个 _system函数&#xff0c;改函数能执行系统命令 既然反编译有这个函数说明有地方调用了他 果然在一个fun函数中有调用&#xff0c;执行的命令是 /bin/sh 也就是一个后门函数&…

C# Linq源码分析之Take(五)

概要 本文在C# Linq源码分析之Take&#xff08;四&#xff09;的基础上继续从源码角度分析Take的优化方法&#xff0c;主要分析Where.Select.Take的使用案例。 Where.Select.Take的案例分析 该场景模拟我们显示中将EF中与数据库关联的对象进行过滤&#xff0c;然后转换成Web…