【C++】红黑树的封装——同时实现map和set

目录

  • 红黑树的完善
    • 默认成员函数
    • 迭代器的增加
  • 红黑树的封装
    • 红黑树模板参数的控制
    • 仿函数解决取K问题
    • 对Key的非法操作
  • insert的调整
  • map的[]运算符重载

在list模拟实现一文中,介绍了如何使用同一份代码封装出list的普通迭代器和const迭代器。今天学习STL中两个关联式容器mapset,其底层就是红黑树,而且是同一棵红黑树。所以今天学习如何用同一棵红黑树封装出mapset

红黑树的封装

红黑树的完善

默认成员函数

map和set的底层是红黑树,所以先将红黑树重要的四个默认成员函数完善好。这四个成员函数在介绍搜索二叉树时已进行过介绍,这里不再讲解。

public://RBTree() = default;RBTree():_root(nullptr){}RBTree(const RBTree& t)//拷贝构造{_root = Copy(t._root);}RBTree& operator=(RBTree t)//赋值重载{swap(_root, t._root);return *this;}~RBTree(){Destroy(_root);_root = nullptr;}
private:Node* Copy(Node* root){if (root == nullptr){return nullptr;}Node* newnode = new Node(root->_kv);newnode->_left = Copy(root->_left);newnode->_right = Copy(root->_right);return newnode;//返回时才连接}void Destroy(Node* root){if (root == nullptr){return;}Destroy(root->_left);Destroy(root->_right);delete root;}

迭代器的增加

map和set作为关联式容器,迭代器是必不可少的,而map和set都是由同一颗红黑树封装而成,所以,map和set的迭代器其实就是红黑树的迭代器。

map和set的迭代器都是双向迭代器,也就是说都支持++,- -的操作。

map迭代器

set迭代器

红黑树的迭代器的结构和list的是类似的,其成员都只有一个节点类,只有一个构造函数初始化节点。

迭代器:

template<class T, class Ref, class Ptr>
struct RBTreeIterator
{typedef RBTreeNode<T> Node;typedef RBTreeIterator<T,Ref,Ptr> self;Node* _node;//成员RBTreeIterator(Node* node):_node(node){}
};

节点类:

template<class T>
struct RBTreeNode
{T _kv;RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Color _col;RBTreeNode(const T& kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED)//默认为红色{}};

对于自封装的迭代器:解引用,是否相等的比较是少不了的,而这部分操作还是比较容易的,不理解的可参考list迭代器的实现

  • 注意此时要访问的的对象是_kv
	Ref operator*(){return _node->_kv;}Ptr operator->(){return &_node->_kv;}bool operator==(const self& s){return _node == s._node;}bool operator!=(const self& s){return _node != s._node;}

真正的难点是红黑是迭代器的遍历:中序遍历红黑树便能得到一个升序序列,而迭代器访问的顺序也是中序:左根右。采用Inorde来遍历是很简单的,但是它是不可控的,只能一把把红黑树全遍历完。而迭代器必须是一个一个节点访问的,这就增加了它实现的难度。

红黑树迭代器

前置++

对于++,即按中序访问下一个节点,但查找时此时的节点已经为根,所以查找的顺序为根,右;此时关注的点是下一个节点是否为空,由此分为两种情况。

  • 右子树不为空;则,右子树的最左节点即为下一个要访问的节点。
  • 右子树为空,说明当前子树已经访问完了;沿着到根节点的路径查找祖先节点中左孩子为父亲的节点即为下一个要访问的节点。
    • 如果父节点为祖先节点的右孩子,说明递归有一定深度,需要不断向上查找。
	self& operator++(){if (_node->_right){Node* leftMost = _node->_right;while (leftMost->_left){leftMost = leftMost->_left;}_node = leftMost;//return self(leftMost);}else//说明当前子树访问完了{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = parent;parent = parent->_parent;}//return self(parent);_node = parent;}return *this;}

前置++可以引用返回。

后置++

后置++逻辑与前置是一样的,只不过返回的是++之前的值。所以用ret保留原先的值,再访问下一个节点,最后返回ret即可。此时不能引用返回。

	self operator++(int){self ret = *this;if (_node->_right){Node* leftMost = _node->_right;while (leftMost->_left){leftMost = leftMost->_left;}_node = leftMost;}else//说明当前子树访问完了{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = parent;parent = parent->_parent;}_node = parent;}return ret;}

前置- -

红黑树的- -即访问当前节点的前一个节点。此时的查找顺序变为根,左;自身就作为根节点,所以此时只需要注意左节点是否为空。

  • 左节点不为空,则按右根左的遍历顺序遍历;访问左子树的最右孩子。
  • 左节点为空,说明当前子树已经访问完了;沿着到根节点的路径查找祖先节点中右孩子为父亲的节点即为下一个要访问的节点。
    • 如果父节点为祖先节点的左孩子,说明递归有一定深度,需要不断向上查找
    self& operator--(){if (_node->_left){Node* rightMost = _node->_left;while (rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else//说明当前子树访问完了{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = parent;parent = parent->_parent;}_node = parent;}return *this;}

后置- -

返回- -之前的节点。

	self operator--(int){Node* ret = *this;if (_node->_left){Node* rightMost = _node->_left;while (rightMost->_right){rightMost = rightMost->_right;}_node = rightMost;}else//说明当前子树访问完了{Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = parent;parent = parent->_parent;}_node = parent;}return ret;}

迭代器实现后,我们需要在红黑树的实现当中进行迭代器类型的typedef。需要注意的是,为了让外部能够使用typedef后的迭代器类型iterator,需要在public区域进行typedef。

然后在红黑树当中实现成员函数begin和end:

  • begin函数返回中序序列当中第一个结点的迭代器,即最左结点。
  • end函数返回中序序列当中最后一个结点下一个位置的迭代器,这里直接用空指针构造一个迭代器。

实现时红黑树一层命名为与map和set作区分,首字母大写。

template<class K, class T,class KeyOfT>//map时T为pair,set时为K
class RBTree
{typedef RBTreeNode<T> Node;
public:typedef RBTreeIterator<T, T&, T*> Iterator;typedef RBTreeIterator<T, const T&, const T*> Const_Iterator;//map和set的迭代器也是使用红黑树的迭代器,但树的结构有所区别。//红黑树的迭代器Iterator Begin(){Node* leftMost = _root;while (leftMost->_left){leftMost = leftMost->_left;}return leftMost;//此时返回的是节点,而返回类型为迭代器,所以会发生隐式类型转化,调用红黑树的迭代器构造函数构造一个迭代器}Iterator End()//与库里的带头红黑树不同,我们这里不带头,为空即尾{return nullptr;}Const_Iterator Begin()const{Node* leftMost = _root;while (leftMost->_left){leftMost = leftMost->_left;}return leftMost;}Const_Iterator End()const//与库里的带头红黑树不同,我们这里不带头,为空即尾{return nullptr;}private:Node* _root = nullptr;};

库中红黑树的结构实现

库里的红黑树结构中还有一个头节点header,header的parent指针指向_root,_root的parent指针指向header,header的左右指针分别指向红黑树的最小和最大节点。

在这里插入图片描述
由于结构不同,所以我们迭代器的实现也有一定缺陷,但不妨碍正常使用,所以就不进行完善了。

红黑树的封装

都知道setkey模型,而mapkey_value模型,如何才能使用同一棵红黑树满足二者的需求呢?这就是封装的魅力。

map

set
了解map和set之后,两者的冲突点主要有:

  1. mapKV模型,setK模型
  2. 获取Key
    • mapset储存数据的方式不一样;红黑树的大多数操作都是需要支持用Key比大小的。
  3. mapsetKey不可修改问题

红黑树模板参数的控制

关于set和map的不同模型问题,先看看库中是如何解决这个问题的。
库中红黑树对map和set的处理
对于红黑树而言,它是不知道上层是map还是set的,但是这些都不重要;底层红黑树利用泛型的思想,将map和set传过来的参数实例化为模板;这样一来,上层map传对应的参数过来,底层红黑树就将这些参数泛化成模板,就能生成对应数据类型的红黑树了;set也是同理。

如简化版的下图:map和set的底层都是一棵红黑树,其中map和set中的红黑树传入的第一个参数都为K;而map的第二个参数传入的是一个键值对pair,这也正是map的数据类型。set的第二个参数继续传入一个K,作为set的数据类型。也正是这样的设计,能够让一棵红黑树同时封装map和set。

这样一来,你上层传的是pair,底层红黑树实例出来的就是map,传入的为K,则为set。

红黑树模板参数的控制

  • 第三个模板参数是解决下一个问题所提供的仿函数。

set中调用的红黑树为什么要传两次K?

  • set中传两次K确实有点多余,但此时是map和set共用一棵红黑树,在map的日常使用中如:find,erase这样的操作,其参数就是一个K类型,所以map中是需要有K的。

  • map_find

  • map_erase

仿函数解决取K问题

所谓取K,就是由于map和set的数据类型不一致,一个是KV的键值对,一个是模板参数K。作为而平衡二叉树的红黑树,Key值的比对是少不了的,如插入,查找等功能都是需要有Key值的比对的。对于set来说,可以直接用传入的K进行比对;而map是pair,需要解引用访问其first才能找到Key,否则直接比对pair不一定是我们想要的比对结果。

pair的比对方式
所以,为了解决这个问题,继续参考上一个问题的解决方式:在map和set调用红黑树传参时传入一个可以取Key的仿函数。仿函数介绍

map的仿函数:要获取是数据是什么类型,仿函数的参数就是什么类型。

		struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};

set的仿函数:set的仿函数看起来有点多此一举,但为了适配map的解决,提供一个仿函数也无妨。

		struct SetKeyOfT{const K& operator()(const K& key){return key;}};

取Key
这样一来,set容器传入底层红黑树的就是set的仿函数,map容器传入底层红黑树的就是map的仿函数。

有了仿函数,当底层红黑树需要进行两个结点之间键值的比较时,都会通过传入的仿函数来获取相应结点的键值,然后再进行比较,下面以红黑树的查找函数为例。

Iterator Find(const T& data){KeyOfT kot;if (_root == nullptr){return nullptr;}Node* cur = _root;//Node* parent = nullptr;while (cur){if (kot(data) > kot(cur->_kv)){//parent = cur;cur = cur->_right;}else if (kot(data) < kot(cur->_kv)){//parent = cur;cur = cur->_left;}else{return cur;}}return nullptr;}

对Key的非法操作

mapset都是不支持修改Key的

map对此的解决方案是pair对中的Key用const修饰
Key修改问题

map成员的定义:

private:RBTree<K, pair<const K, V>, MapKeyOfT> _rbt;

set将红黑树的第二个参数改为const K好像也能解决问题,但库里并没有采用这种方法。

简易办法:使用const K禁止被修改。目前还没发现什么大问题,如果发现问题请告知
set成员的定义:

	private:RBTree<K,const K, SetKeyOfT> _rbt;

第二种就是采用库里的方法。
set无论普通还是const迭代器都使用红黑树的const迭代器

public://库里的方法:typedef typename RBTree<K, K, SetKeyOfT>::Const_Iterator iterator;typedef typename RBTree<K, K, SetKeyOfT>::Const_Iterator const_iterator;

但是这样就会出现类型转化问题
类型转化
原因如下:

这里的迭代器不是原生指针的迭代器,而是通过封装而来的,借助模板实现了const和非const版的迭代器;也就导致普通的无法转为const迭代器;通过调试发现问题出现在获取迭代器上,由于set的普通迭代器也由const迭代器封装而来,set在获取普通迭代器时,最底层的红黑树返回的是普通迭代器,但set的普通迭代器实际为const迭代器,此时就发生了类型不匹配的问题。

对此,库中提供了如下解决方案:在红黑树迭代器中提供一个构造函数

set的Key问题
在红黑树迭代器中增加一个参数为普通迭代器类型的构造函数。该构造函数取参数中普通迭代器的节点重新构造一个迭代器(const版)。该方法绕过转化,采用构造,生成一个const版的迭代器,自然就没有类型转化的问题。

虽然这个构造函数增加在红黑树的迭代器中,但是map的迭代器不会有影响,这个构造函数只会匹配到set对应的状况。

	typedef RBTreeIterator<T, T&, T*> Iterator;	//声明类型:普通迭代器RBTreeIterator(const Iterator& it)//类型为普通迭代器,因为就是由于普通迭代器转化为const迭代器这一环出了                   问题,这里专门针对这一情况处理:_node(it._node)//此时需要的是一个const迭代器,(由于模板无法转化?)普通转const直接转化不行,那么我们就在返回时利用隐式类型转化{}

具体过程请看下图。
类型转化问题

insert的调整

在AVL树红黑树阶段实现的insert的返回值都为bool,在map和set中则是改为返回键值对pair。

以map为例:
insert介绍

函数声明:

pair<iterator,bool> insert (const value_type& val);

可以看到其返回值为pair,pair的第一个成员为一个迭代器指向新插入的元素,或者已存在的等效元素,第二个成员为bool,用来检测插入是否成功;也就是说insert成功的话会返回一个指向新插入元素的迭代器,其bool值为true的pair,如果insert失败则会返回一个指向容器中原有的K的迭代器,其bool值为false的pair。

map的[]运算符重载

map支持[]访问容器中Key值并返回Key对应的Value;set不支持[]重载,因为只有一个Key。
[]使用

也就是说[]的参数为pair中的第一个成员K,其使用说明如下:

如果K与容器中某个元素的键匹配,则该函数返回K值关联的Value引用。如果K与容器中任何元素的键不匹配,则该函数用该键插入一个新元素,并返回对其映射值的引用。这个新元素会有默认值

调用[]类似于下面的操作:

(*((this->insert(make_pair(k,mapped_type()))).first)).second

也就是说[]的实现是借助于insert实现的。而这样的话[]的用法就有两种:

  1. 插入
    • 如果K是map中不存在的,那么这就是一个插入操作;由于其返回值为第二个模板参数value的引用,所以可以直接修改。
    • []使用1
  2. 修改
    • 如果K是map中已有的值,那么insert将会返回已存在K的迭代器,[]最终返回一个K的引用,相当于支持修改操作。
    • []使用2

所以[]重载运算符的实现如下。

		//返回value的引用V& operator[](const K& key){pair<iterator, bool> ret = _rbt.Insert({ key,V() });return ret.first->second;//不理解返回second的结合operator->看}
  • 返回值类型为第二个参数的引用,所以支持直接修改。
  • 关于返回V&为何是返回it.first->second:it 为接受的是insert返回的pair对,其first为指向元素的迭代器,->调用了迭代器的operator->,此时返回的是存储KV的pair,此时的second为V

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

lime使用记录

主要是对预测结果进行可解释 import numpy as np import pandas as pd from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_re…

thinkphp6调用微信商户支付-合单支付工具代码开发

合单支付基本在加盟店或是分公司或是营销系统里面常见。他的出现&#xff0c;打破了传统提现支付或是转账支付。他的业务原理其实很简单&#xff0c;就是需要优先申请非普通商户&#xff0c;其次是每个入驻的商户都需要申请普通商户。在这之前一定要申请好对应的场景服务&#…

大学学校用电安全远程监测预警系统

1.概述&#xff1a; 该系统是基于移动互联网、云计算技术&#xff0c;通过物联网传感终端&#xff0c;将办公建筑、学校、医院、工厂、体育场馆、宾馆、福利院等人员密集场所的电气安全数据&#xff0c;实时传输至安全用申管理服务器&#xff0c;为用户提供不间断的数据跟踪&a…

2024年项目经理不能错过的开源项目管理系统大盘点:全面指南

在2024年&#xff0c;随着项目管理领域的不断发展&#xff0c;开源项目管理系统成为了项目经理们提升工作效率的利器。本文将全面盘点几款备受推荐的开源项目管理系统&#xff0c;帮助项目经理们找到最佳选择&#xff0c;不容错过。 在项目管理日益复杂的今天&#xff0c;开源项…

鼎阳加油-IOC关键技术问题的解决记

鼎阳SDS6204示波器EPICS IOC的搭建-CSDN博客 这款示波器在labview下工作的很好&#xff0c;以前搭建逐束团3D系统时连续几个月不间断的工作连接从没断过线&#xff0c;并做过速率测试&#xff0c;单通道时10Hz的波形更新速率都可以达到&#xff1a; 鼎阳SDS6204示波器波形读取…

温州大麓青年音乐节即将开唱,37组音乐人国庆齐聚共谱华章

金秋十月&#xff0c;当丰收的季节与音乐的旋律相遇&#xff0c;温州将迎来一场前所未有的文化盛事。2024年10月1日至4日&#xff0c;温州大麓青年音乐节将在瓯海盛大举行。不仅是一场音乐的狂欢&#xff0c;更是一次多元文化的碰撞与融合。本届音乐节邀请了37组以上的知名音乐…

中级职称评审到底需要准备什么材料?

职称评审需要的材料非常非常多&#xff0c;其中涉及到各类表格&#xff0c;这些小资料&#xff0c;看起来简单&#xff0c;实则做起来复杂&#xff0c;不过这种资料只能当年通知出来之后进行整理&#xff0c;今天甘建二跟大家说一下职称评审中需要提前准备的一些重要材料&#…

酒店智能门锁SDK接口通用转换函数对接酒店收银-SAAS本地化-未来之窗行业应用跨平台架构

一、通用转换代码 public class CyberWin_LocakAPP{// public static byte[] bufCard new byte[128 1];public static string 未来之窗_美萍_getsign(byte[] bufCard){int i;string 酒店标识, s, s2;// 先读卡string 未来之窗 Encoding.ASCII.GetString(bufCard);// edt_Ca…

使用dayjs获取今天日期,星期几

<div>{{ curDate }} {{ getWeek() }}</div>import dayjs from dayjs;data(){return{curDate: dayjs(new Date()).format(YYYY年MM月DD日)} }, mounted() {this.getWeek(); }, methods: {// 获取今天星期几getWeek() {let datas dayjs().day();let week [日, 一, …

Linux 搭建与使用yolov5训练和检验自建模型的步骤

Linux 搭建与使用yolov5训练和检验自建模型的步骤 硬件设备 环境搭建(无cuda) 下载anaconda wget https://repo.anaconda.com/archive/Anaconda3-2024.06-1-Linux-x86_64.sh bash Anaconda3-2024.06-1-Linux-x86_64.sh # 看完许可证后, yes, 后面选择安装路径, 可以按默认路…

打造高效合同管理平台,提升企业运营效率

在现代企业的日常运营中&#xff0c;合同管理扮演着至关重要的角色。无论是劳动协议、采购订单还是销售合同&#xff0c;各类合同都承载着企业的重要信息和业务节点。然而&#xff0c;面对日益复杂的商业环境和海量合同数据&#xff0c;如何有效管理和利用这些合同成为众多企业…

一些写论文必须要知道的神仙级网站!芝士AI(paperzz)

说实话&#xff0c;写论文真的是挺头疼&#xff0c;尤其到了毕业季的时候&#xff0c;没有过任何写作毕业论文的经验的毕业生而言更是如此&#xff0c;相信大家都有过这种状态&#xff0c;不知从何下笔&#xff0c;还需要面对论文进度的压力&#xff0c;并且时常需要寻找各种资…

基于Python大数据可视化的短视频推荐系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码 精品专栏&#xff1a;Java精选实战项目…

刘诗诗现身上海参加可隆自然而然露营节,户外风活力清新生图绝美!

9月26日&#xff0c;刘诗诗现身上海可隆自然而然露营节&#xff0c;活动现场&#xff0c;刘诗诗身着可隆OBLI-K露营冲锋衣外套&#xff0c;经典石库门配色既高级又具有质感&#xff0c;内里搭配简单白T与浅灰色短裙&#xff0c;户外运动风完美拿捏&#xff0c;所穿的鞋子是可隆…

vue单点登录异步执行请求https://xxx.com获取并处理数据

一、请求一个加密地址获取access_token再拼接字符串再次请求 接口返回数据 异步执行请求该地址获取数据并处理 二、请求代码第二步使用 access_token 获取 auth_key // 第二步&#xff1a;使用 access_token 获取 auth_keyconst access_token tokenData.access_token;const …

通配符与Powershell

通配符与正则表达式 通配符 通配符是一种特殊的语句&#xff0c;主要有*、?和[]&#xff0c;用来模糊搜索文件。 通配符表达意思举例说明*星号、匹配任何字符*.cpp匹配.cpp文件?问号、匹配任意一个字符*.?d匹配具有特定格式的文件[]中括号、匹配括号中的一个字符.[a-z]d代…

原生APP开发成本计算

原生APP开发的成本是一个复杂的问题&#xff0c;受到众多因素的影响&#xff0c;很难给出一个精确的数字。但我们可以通过了解影响成本的因素&#xff0c;以及常见的估算方法&#xff0c;来对开发成本有一个大致的了解。 影响原生APP开发成本的因素 功能复杂度&#xff1a; 功…

AI视频技术:引领影视剧拍摄的未来

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 当科技遇见艺术&#xff0c;一场视听盛宴正…

如何正确连接和使用滑动变阻器?

滑动变阻器是可以改变电阻值的电子元件&#xff0c;广泛应用于各种电子设备和电路中。正确连接和使用滑动变阻器对于保证电路的正常工作和延长设备的使用寿命至关重要。以下是关于如何正确连接和使用滑动变阻器的一些建议&#xff1a; 了解滑动变阻器的基本原理和结构&#xf…

Linux上的C/C++编程

Linux上的C/C编程 yum软件包管理器Linux编辑器-vimvim命令模式指令集vim末行模式指令集 gcc/g的使用Linux自动化编译工具-make/MakefileLinux调试器-gdb调试命令 多人合作工具git yum软件包管理器 yum 是Linux上常用的包管理器&#xff0c;类似于Windows上的“应用商店”。 语…