【C++】unordered_set、unordered_map超详细封装过程,处理底层细节

头像
🚀个人主页:@小羊
🚀所属专栏:C++
很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~

动图描述

目录

  • 前言
    • 1、数据泛型
    • 2、迭代器
      • 2.1 ++重载
      • 2.2 begin、end
      • 2.3 const迭代器
      • 2.4 unordered_map中[]重载
    • 3、特殊类型
  • 总结


前言

上篇文章我们简单地实现了哈希表,本篇文章将基于开散列实现的哈希表封装出unordered_setunordered_map的基本功能。
本文不再从头实现哈希表,而是着重介绍封装unordered_setunordered_map中的细节问题,如果小伙伴对哈希表的实现还不太熟悉的话请先阅读上篇文章。


1、数据泛型

基于封装setmap的经验,我们首先把哈希表中节点的模版参数修改一下,用于存储不同类型的Kpair<K, V>,同时底层的代码也要做相应的修改。

namespace hash_bucket
{template<class K>struct HashFunc{size_t operator()(const K& key){return (size_t)key;}};template<>struct HashFunc<string>{size_t operator()(const string& s){size_t hash = 0;for (auto e : s){hash = hash * 31 + e;}return hash;}};template<class T>struct HashNode{HashNode(const T& data):_data(data),_next(nullptr){}T _data;HashNode<T>* _next;};template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>class HashTable{typedef HashNode<T> Node;public:HashTable(){//提前开10个位置,多次扩容_tables.resize(10, nullptr);}~HashTable(){for (int i = 0; i < _tables.size(); i++){Node* pcur = _tables[i];while (pcur){Node* next = pcur->_next;delete pcur;pcur = next;}_tables[i] = nullptr;}}bool Insert(const T& data){Hash hs;KeyOfT kot;//扩容if (_n == _tables.size()){vector<Node*> newtables(2 * _tables.size(), nullptr);for (int i = 0; i < _tables.size(); i++){Node* pcur = _tables[i];while (pcur){size_t hashi = hs(kot(pcur->data)) % newtables.size();Node* next = pcur->_next;pcur->_next = newtables[hashi];newtables[hashi] = pcur;pcur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}size_t hashi = hs(kot(data)) & _tables.size();//头插Node* newnode = new Node(data);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}Node* Find(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _tables.size();Node* pcur = _tables[hashi];while (pcur){if (kot(pcur->_data) == key){return pcur;}pcur = pcur->_next;}return End();}bool Erase(const K& key){Hash hs;KeyOfT kot;size_t hashi = hs(key) % _tables.size();Node* pcur = _tables[hashi];Node* prev = nullptr;while (pcur){if (kot(pcur->_data) == key){if (prev == nullptr){_tables[hashi] = pcur->_next;}else{prev->_next = pcur->_next;}delete pcur;--_n;return true;}prev = pcur;pcur = pcur->_next;}return false;}private:vector<Node*> _tables;size_t _n = 0;//哈希表中实际元素个数};
}

这里的析构函数不能用默认生成的析构函数,虽然vector会调用它的析构函数,但是其中的节点确不能被释放,因此还需要我们手动地进行释放。只需要遍历哈希表,如果有节点先记录下一个节点的地址,再释放,直到遍历完表。


2、迭代器

unordered_setunordered_map迭代器的实现,是封装unordered_setunordered_map的重中之重,也是比较复杂的地方。

template<class T>
struct HTIterator
{typedef HashNode<T> Node;typedef HTIterator<T> Self;HTIterator(Node* node):_node(node){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}bool operator!=(const Self& s){return _node != s._node;}Node* _node;
};

2.1 ++重载

你可能会想,哈希表中哈希桶是一个链表,只需要pcur = pcur->_next就能得到下一个节点的迭代器,如果你真这样想我不禁要发出灵魂拷问:如果当前迭代器是当前桶的最后一个节点呢?

头像

所以说,哈希表迭代器++前有两种情况:

  1. 当前迭代器不是当前桶的最后一个节点
  2. 当前迭代器当前桶的最后一个节点

我们都知道第一种情况倒是好解决,但是第二种情况就很让人挠头。因为单就论两个链表而言,我们无法直接从一个链表上走到另一个链表上,这就阻挡了迭代器想要前进的脚步,怎么办呢?

头像

聪明的你肯定注意到了我们说的是无法直接走,那我们就不直接走呗。单论两个链表确实找不到交集,但别忘了无论它们两个相距多远,哪怕相隔银河,它们也始终都在同一个哈希表中,所以当一个链表走到头时,我们可以借助哈希表找到下一个不为空的链表
但是当前的迭代器中并没有哈希表,这也就意味着我们的迭代器中还需要有一个哈希表的指针(对象也可以,不过相对麻烦一点)。

//前置声明
template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable;template<class K, class T, class KeyOfT, class Hash>
struct HTIterator
{typedef HashNode<T> Node;typedef HTIterator<K, T, KeyOfT, Hash> Self;HTIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pth):_node(node),_pht(pht){}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}bool operator!=(const Self& s){return _node != s._node;}Self& operator++(){}Node* _node;HashTable<K, T, KeyOfT, Hash>* _pht;
};
  • 这一步需要注意的反而是模版参数的对应问题

上面的代码中哈希表和迭代器有相互相互依赖的问题,因为我们的哈希表和迭代器肯定是定义一个在前一个在后,而我们知道编译器只会向上查找,所以不管谁定义在前面都不可避免,解决这个问题需要前置声明

两种情况我们都有了应对之策,接下来就着手重载++。如果当前桶还没有走完,就返回下一个节点的迭代器;如果当前桶走完了,先通过迭代器指向的节点确定当前桶在哈希表中的映射位置,然后向后走找第一个不为空的桶,第一个不为空的桶的头节点就是我们要找的节点。
这里还需要处理一个特殊情况,就是后面的桶都为空,此时迭代器++得到end()

Self& operator++()
{//当前桶不为空if (_node->_next){_node = _node->_next;}else//当前桶已空{Hash hs;KeyOfT kot;size_t hashi = hs(kot(_node->data)) % _pht->_tables.size();++hashi;while (hashi < _pht->_tables.size()){if (_pht->_tables[hashi]){break;}++hashi;}if (hashi == _pht->_tables.size()){_node = nullptr;}else{_node = _pht->_tables[hashi];}}return *this;
}

如果你用上面的代码去测试会发现还是跑不通,哪里又有问题呢?通过报错不难发现,问题出现在哈希表中的 _tables是一个私有成员,在哈希表外是不能直接访问的,解决这个问题也简单,只需要将迭代器作为哈希表的友元类即可。

友元的类模版声明时需要带上模版参数。


2.2 begin、end

返回哈希表的起始迭代器,只需要遍历哈希表找到哈希表的第一个不为空的桶,桶中的头节点的迭代器就是哈希表的起始迭代器。如果哈希表中没有数据就不需要遍历哈希表了。end迭代器我们还是用nullptr构造。
构造迭代器除了传节点指针外,还需要传哈希表的指针,那哈希表的指针怎么传呢?没错,在哈希表中this就是哈希表的指针。

Iterator Begin()
{if (_n == 0){return End();}for (int i = 0; i < _tables.size(); i++){Node* pcur = _tables[i];if (pcur){return Iterator(pcur, this);}}return End();
}Iterator End()
{return Iterator(nullptr, this);
}

2.3 const迭代器

const迭代器还是和红黑树的封装一样,增加两个模版参数来实现对普通迭代器类的复用。

template<class K, class T, class Ptr, class Ref, class KeyOfT, class Hash>
struct HTIterator
{typedef HashNode<T> Node;typedef HTIterator<K, T, Ptr, Ref, KeyOfT, Hash> Self;HTIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht):_node(node),_pht(pht){}//...
}template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{//友元声明template<class K, class T, class Ptr, class Ref, class KeyOfT, class Hash>friend struct HTIterator;typedef HashNode<T> Node;
public:typedef HTIterator<K, T, T*, T&, KeyOfT, Hash> Iterator;typedef HTIterator<K, T, const T*, const T&, KeyOfT, Hash> ConstIterator;//...ConstIterator Begin() const{if (_n == 0){return End();}for (int i = 0; i < _tables.size(); i++){Node* pcur = _tables[i];if (pcur){return ConstIterator(pcur, this);}}return End();}ConstIterator End() const{return ConstIterator(nullptr, this);}private:vector<Node*> _tables;size_t _n = 0;
};

const迭代器完成后我们用下面的函数测试一下:

void Print(const unordered_set<int>& s)
{unordered_set<int>::const_iterator it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;
}

编译运行还是有问题,原因是上面的begin返回的是const迭代器,其函数内部的成员都是const成员,包括哈希表,所以其this指针也应该是被const修饰的,但是我们实现的迭代器的构造函数形参中哈希表的指针并没有const修饰,有权限放大的错误。具体如下图所示:

在这里插入图片描述

因此下面这两个地方都需要const修饰才行。

在这里插入图片描述

和set、map一样,unordered_set、unordered_map的key同样不能修改,这里也可以仿照set和map的封装一样给单独K加上const修饰就行。


2.4 unordered_map中[]重载

map中的[]重载是复用的insert函数,主要是利用其返回值,unordered_map也不例外。迭代器实现的差不多后我们将Find、Insert等函数的返回值就可以完善了。

pair<Iterator, bool> Insert(const T& data)
{KeyOfT kot;if (Find(kot(data)) != End()){return make_pair(Find(kot(data)), false);}Hash hs;size_t hashi = hs(kot(data)) % _tables.size();//负载因子==1就扩容if (_n == _tables.size()){vector<Node*> newtables(2 * _tables.size(), nullptr);for (int i = 0; i < _tables.size(); i++){Node* pcur = _tables[i];while (pcur){Node* next = pcur->_next;//记录下一个节点size_t hashi = hs(kot(pcur->_data)) % newtables.size();//映射新表的相对位置pcur->_next = newtables[hashi];//头插newtables[hashi] = pcur;pcur = next;}_tables[i] = nullptr;}_tables.swap(newtables);}Node* newnode = new Node(data);//头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return make_pair(Iterator(newnode, this), true);
}Iterator Find(const K& key)
{KeyOfT kot;Hash hs;size_t hashi = hs(key) % _tables.size();Node* pcur = _tables[hashi];while (pcur){if (key == kot(pcur->_data)){return Iterator(pcur, this);}pcur = pcur->_next;}return End();
}

最后重载[],[]的调用等价于:

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

key存在,返回对应的value;key不存在,插入key和value(默认)。
所以我们可以复用insert函数插入新元素,然后不管是否插入成功都返回迭代器的second

V& operator[](const K& key)
{pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;
}

3、特殊类型

其实上面我们只考虑了整型、浮点型、字符串等做key的情况,如果key是一个特殊类型,比如我们熟悉的日期类,则上面的取模操作还是有问题的,并且还不是再实现一个仿函数的问题,而是我们的包装有问题。

在这里插入图片描述

问题就出现在这里,小伙伴们可以思考一下我们能在这里给缺省值吗?
是不可以的,因为我们现在是在实现封装,因此不可能越过unordered_setunordered_map去直接操作哈希表,那在这里给缺省值就写死了,当遇到日期类这种特殊类型时我们需要自己实现相应的仿函数来支持取模,而我们在哈希表内部实现了无符号整型的强转和字符串的整形变化是因为它们都是非常常见的。

在这里插入图片描述

我们应该在unordered_setunordered_map的层面加仿函数的缺省值,这样如果遇到日期类这种特殊类型的需求,我们就可以按需传仿函数完成整型的转换。

另外为了防止像1月2号和2月1号这种产生冲突的情况,可以仿照字符串哈希函数的处理方法,给年月日乘以31这样的特殊数字来减少冲突。

在这里插入图片描述


总结

  • unordered_setunordered_map的封装相较于setmap的封装还是相对较复杂的,其中复杂之处主要在于模版参数间的对应关系,如果某处做修改一般都会牵扯到多个地方,因此封装时必须时刻清晰各个板块之间的依赖关系。
  • 一些不支持修改也就是const修饰的地方,往往还存在着权限放大的问题,也要时刻小心。

本篇文章的分享就到这里了,如果您觉得在本文有所收获,还请留下您的三连支持哦~

头像

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

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

相关文章

如何设计开发RTSP直播播放器?

技术背景 我们在对接RTSP直播播放器相关技术诉求的时候&#xff0c;好多开发者&#xff0c;除了选用成熟的RTSP播放器外&#xff0c;还想知其然知其所以然&#xff0c;对RTSP播放器的整体开发有个基础的了解&#xff0c;方便方案之作和技术延伸。本文抛砖引玉&#xff0c;做个…

Unity 实战案例全解析 实现时间停止效果+世界变灰

画面里运动的那个小玩意这么写 using System.Collections; using System.Collections.Generic; using UnityEngine;public class Partol : MonoBehaviour {public Transform pos1;public Transform pos2;public float speed;private Transform target;void Start() {target p…

前端开发攻略---取消已经发出但是还未响应的网络请求

目录 注意&#xff1a; 1、Axios实现 2、Fetch实现 3、XHR实现 注意&#xff1a; 当请求被取消时&#xff0c;只会本地停止处理此次请求&#xff0c;服务器仍然可能已经接收到了并处理了该请求。开发时应当及时和后端进行友好沟通。 1、Axios实现 <!DOCTYPE html> &…

性能测试工具JMeter

本次使用的博客系统的url&#xff1a; http://8.137.19.140:9090/blog_edit.html 1. JMeter介绍 环境要求&#xff1a;要求java&#xff0c;jdk版本大于8&#xff1b; Apache JMeter 是 Apache 组织基于 Java 开发的压⼒测试⼯具&#xff0c;⽤于对软件做性能测试&#xff1b…

带新手用一套坦克大战搞定Java核心编程

有不少在校大学生朋友私信笔者&#xff0c;觉得现在的Java教程学的比较枯燥&#xff0c;有没有能学到基础知识同时还有做项目感觉的教程。因为笔者最早接触Java时学的是马士兵老师、韩顺平老师这样的大佬当时出的教程&#xff0c;尤其印象深刻的是二位大佬的坦克大战项目&#…

基于DSP+ARM+FPGA的电能质量分析仪的软件设计

软件设计是电能质量设备的核心内容&#xff0c;上述章节详细介绍了电能质量参数的 算法&#xff0c;并且通过仿真实验进行了验证&#xff0c;本章将结合现代电能质量监测设备需求实 现算法在实际电网中应用。根据设计的电能质量分析仪的需求分析&#xff0c;进行总体的 软件…

Java筑基之路:运算符深入(下)

&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d;&#x1f51d; &#x1f947;博主昵称&#xff1a;小菜元 &#x1f35f;博客主页…

计算机网络:数据链路层 —— 共享式以太网

文章目录 共享式以太网CSMA/CD 协议CSMA/CD 协议 的基本原理 共享式以太网的争用期共享式以太网的最小帧长共享式以太网的最大帧长共享式以太网的退避算法截断二进制指数退避算法 共享二进制以太网的信道利用率使用集线器的共享式以太网10BASE-T 共享式以太网 共享式以太网是当…

51单片机数码管循环显示0~f

原理图&#xff1a; #include <reg52.h>sbit dulaP2^6;//段选信号 sbit welaP2^7;//位选信号unsigned char num;//数码管显示的数字0~funsigned char code table[]{ 0x3f,0x06,0x5b,0x4f, 0x66,0x6d,0x7d,0x07, 0x7f,0x6f,0x77,0x7c, 0x39,0x5e,0x79,0x71};//定义数码管显…

yakit使用教程(四,信息收集)

本文仅作为学习参考使用&#xff0c;本文作者对任何使用本文进行渗透攻击破坏不负任何责任。 前言&#xff1a;yakit下载安装教程。 一&#xff0c;基础爬虫。 在新建项目或新建临时项目后&#xff0c;点击安全工具&#xff0c;点击基础爬虫。 此工具并不是为了爬取网站上的一…

navicat下载教程(包会的)

目录 一、下载navicat安装包 步骤1---试用版本 步骤2---下载windws系统的navicat​编辑 步骤3---查看安装包 二、安装navicat 三、唤醒navicat 四、成功唤醒navicat 官网地址&#xff1a;Navicat | 下载 Navicat Premium 14 天免费 Windows、macOS 和 Linux 的试用版 …

[Linux] 创建可以免密登录的SFTP用户

本文主要包含: 创建新用户创建密钥对用于免密登录新用户将新建用户改造为SFTP用户为SFTP上传数据设置限速 1. 创建新用户 sudo useradd sftp_user sudo passwd sftp_user # 输入密码2. 创建密钥对 参考这篇文章 [Linux] 生成 PEM 密钥对实现服务器的免密登录 3. 将新建用户…

牛客习题—线性DP 【mari和shiny】C++

你好&#xff0c;欢迎阅读我的文章~ 个人主页&#xff1a;Mike 所属专栏&#xff1a;动态规划 mari和shiny mari和shiny ​ 分析: 使用动态规划的思路来解决。 思路&#xff1a; 分别统计s&#xff0c;sh&#xff0c;shy的数量即可。使用ss来统计字符s的数量&#xff0c;使…

ORA-65096:公用用户名或角色名无效

CREATE USER DATA_SHARING IDENTIFIED BY "Ab2"; Oracle建立用户的的时候&#xff0c;可能会出现一直提示 ORA-65096:公用用户名或角色名无效&#xff1b; 我查了一下&#xff0c;好像是 oracle 12版本及以上版本的特性&#xff0c;用户名必须加c##或者C##前缀才能创…

三勾软件/ java+springboot+vue3玖玖云电商ERP多平台源码

玖玖云ERP系统、支持多平台订单同步&#xff0c;仓库发货&#xff0c;波次拣货&#xff0c;售后服务&#xff0c;电商ERP一站式解决方案 项目介绍 玖玖云ERP系统基于javaspringbootelement-plusuniapp打造的面向开发的电商管理ERP系统&#xff0c;方便二次开发或直接使用。主…

FFmpeg的简单使用【Windows】

目录 一、视频生成图片 静态图片 转码过程 动态图片gif 二、图片生成视频 三、FFmpeg常用参数命令 3.1 主要参数 3.1.1、-i 3.1.2、-f 3.1.3、-ss 3.1.4、-t 3.2 音频参数 3.2.1、-aframes 3.2.2、 -b:a 3.2.3、-ar 3.2.4、-ac 3.2.5、-acodec 3.2.6、-an 3…

HarmonyOS中ArkUi框架中常用的装饰器

目录 1.装饰器 1&#xff09;Component 1--装饰内容 2&#xff09;Entry 1--装饰内容 2--使用说明 3&#xff09;Preview 1--装饰内容 2--使用说明 4&#xff09;CustomDialog 1--装饰内容 2--使用说明 5&#xff09;Observed 1--装饰内容 2--使用说明 6&#xff09;ObjectLin…

【实战篇】用SkyWalking排查线上[xxl-job xxl-rpc remoting error]问题

一、组件简介和问题描述 SkyWalking 简介 Apache SkyWalking 是一个开源的 APM&#xff08;应用性能管理&#xff09;工具&#xff0c;专注于微服务、云原生和容器化环境。它提供了分布式追踪、性能监控和依赖分析等功能&#xff0c;帮助开发者快速定位和解决性能瓶颈和故障。…

【机器学习(十三)】零代码开发案例之股票价格预测分析—Sentosa_DSML社区版

文章目录 一、背景描述二、Sentosa_DSML社区版算法实现(一) 数据读入(二) 特征工程(三) 样本分区(四) 模型训练和评估(五) 模型可视化 三、总结 一、背景描述 股票价格是一种不稳定的时间序列,受多种因素的影响。影响股市的外部因素很多,主要有经济因素、政治因素和公司自身因素…

ASP.NET Core8.0学习笔记(二十)——EFCore导航属性与外键

一、什么是实体间关系 数据库表&#xff08;实体&#xff09;之间的关系&#xff1a;一对一&#xff08;学生-成绩&#xff09;、一对多&#xff08;学生-科目&#xff09;、多对多&#xff08;教师-班级&#xff09;。数据库中&#xff0c;每一个实体可以由主键唯一标识&…