【C++、数据结构】封装unordered_map和unordered_set(用哈希桶实现)
文章目录
- 📖 前言
- 1. 复用同一个哈希桶⚡
- 1.1 🌀修改后结点的定义
- 1.2 🌀两个容器各自模板参数类型:
- 2. 改造之后的哈希桶⛳
- 3. 哈希桶的迭代器🔥
- 3.1 💥哈希桶的begin()和 end()的定义
- 3.2 💥 operator* 和 operator->
- 3.3 💥 operator++
- 3.4 💥 operator== 和 operator!=
- 4. 封装unordered_map和unordered_set⭕
📖 前言
与学习红黑树和map、set的思路一样,我们在学unordered_map和unordered_set时,也是先学底层结构,在用模拟的底层结构来自己封装一下该容器,动手实践来让我们更好的学习和理解底层逻辑。
前情回顾:哈希桶 👉 传送门
这里用到的封装思路和封装map、set的思路相同,都是更高维度的泛型编程。
思路复习:封装map和set 👉 传送门
1. 复用同一个哈希桶⚡
如何复用同一个哈希桶,我们就需要对哈希桶进行改造,将哈希桶改造的更加泛型一点,既符合Key模型,也符合Key_Value模型。
1.1 🌀修改后结点的定义

所以我们这里还是和封装map和set时一样,无论是Key还是Key_Value,都用一个类型T来接收,这里高维度的泛型哈希表中,实现还是用的是Kye_Value模型,K是不能省略的,同样的查找和删除要用。
1.2 🌀两个容器各自模板参数类型:

如何取到想要的数据:
- 我们给每个容器配一个仿函数
- 各传不同的仿函数,拿到想要的不同的数据
同时我们再给每个容器配一个哈希函数。
2. 改造之后的哈希桶⛳
//K --> 键值Key,T --> 数据
//unordered_map ->HashTable<K, pair<K, V>, MapKeyOfT> _ht;
//unordered_set ->HashTable<K, K, SetKeyOfT> _ht;
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{template<class K, class T, class KeyOfT, class HashFunc>friend class __HTIterator;typedef HashNode<T> Node;
public:typedef __HTIterator<K, T, KeyOfT, HashFunc> iterator;iterator begin(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];if (cur){return iterator(cur, this);}}return end();}iterator end(){return iterator(nullptr, this);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;delete cur;cur = next;}_tables[i] = nullptr;}}size_t GetNextPrime(size_t prime){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53, 97, 193, 389, 769,1543, 3079, 6151, 12289, 24593,49157, 98317, 196613, 393241, 786433,1572869, 3145739, 6291469, 12582917, 25165843,50331653, 100663319, 201326611, 402653189, 805306457,1610612741, 3221225473, 4294967291};//获取比prime大那一个素数size_t i = 0;for (i = 0; i < PRIMECOUNT; i++){if (primeList[i] > prime)return primeList[i];}return primeList[i];}pair<iterator, bool> Insert(const T& data){HashFunc hf;KeyOfT kot;iterator pos = Find(kot(data));if (pos != end()){return make_pair(pos, false);}//负载因子 == 1 扩容 -- 平均每个桶挂一个结点if (_tables.size() == _n){//size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;size_t newSize = GetNextPrime(_tables.size());if (newSize != _tables.size()){vector<Node*> newTable;newTable.resize(newSize, nullptr);//遍历旧表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];//再对每个桶挨个遍历while (cur){Node* next = cur->_next;size_t hashi = hf(kot(cur->_data)) % newSize;//转移到新的表中cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}//将原表置空_tables[i] = nullptr;}newTable.swap(_tables);}}size_t hashi = hf(kot(data));hashi %= _tables.size();//头插到对应的桶即可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){if (_tables.size() == 0){return iterator(nullptr, this);}KeyOfT kot;HashFunc hf;size_t hashi = hf(key);//size_t hashi = HashFunc()(key);hashi %= _tables.size();Node* cur = _tables[hashi];//找到指定的桶之后,顺着单链表挨个找while (cur){if (kot(cur->_data) == key){return iterator(cur, this);}cur = cur->_next;}//没找到返回空return iterator(nullptr, this);}bool Erase(const K& key){if (_tables.size() == 0){return false;}HashFunc hf;KeyOfT kot;size_t hashi = hf(key);hashi %= _tables.size();//单链表删除结点Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (kot(cur->_data) == key){//头删if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}
private://指针数组vector<Node*> _tables;size_t _n = 0;
};
研究表明:除留余数法,最好模一个素数
- 通过查STL官方库我们也发现,其提供了一个取素数的函数
- 所以我们也提供了一个,直接拷贝过来
-
- 这样我们在扩容时就可以每次给素数个桶
-
- 在扩容时加了一条判断语句是为了防止素数值太大,过分扩容容易直接把空间(堆)干崩了
3. 哈希桶的迭代器🔥
3.1 💥哈希桶的begin()和 end()的定义

- 以第一个桶中第一个不为空的结点为整个哈希桶的开始结点
- 以空结点为哈希桶的结束结点
3.2 💥 operator* 和 operator->

同之前operator->的连续优化一样,不再赘述……
3.3 💥 operator++

备注:
- 这里要在哈希桶的类外面访问其私有成员
- 我们要搞一个友元类
- 迭代器类是哈希桶类的朋友
- 这样就可以访问了

思路:
- 判断一个桶中的数据是否遍历完
-
- 如果所在的桶没有遍历完,在该桶中返回下一个结点指针
-
- 如果所在的桶遍历完了,进入下一个桶
- 判断下一个桶是否为空
-
- 非空返回桶中第一个节点
-
- 空的话就遍历一个桶
后置++和之前一眼老套路,不赘述
注意:
- unordered_map和unordered_set是不支持反向迭代器的,从底层结构我们也能很好的理解(单链表找不了前驱)
- 所以不支持实现迭代器的operator- -
3.4 💥 operator== 和 operator!=

template<class K, class T, class KeyOfT, class HashFunc>
class HashTable;//哈希桶的迭代器
template<class K, class T, class KeyOfT, class HashFunc>
class __HTIterator
{typedef HashNode<T> Node;typedef __HTIterator<K, T, KeyOfT, HashFunc> Self;
public:Node* _node;__HTIterator() {};//编译器的原则是向上查找(定义必须在前面,否则必须先声明)HashTable<K, T, KeyOfT, HashFunc>* _pht;__HTIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht):_node(node), _pht(pht){}Self& operator++(){if (_node->_next){ _node = _node->_next;}else//当前桶已经走完了,要走下一个桶{KeyOfT kot;HashFunc hf;size_t hashi = hf(kot(_node->_data)) % _pht->_tables.size();hashi++;//找下一个不为空的桶 -- 访问到了哈希表中私有的成员(友元)for (; hashi < _pht->_tables.size(); hashi++){if (_pht->_tables[hashi]){_node = _pht->_tables[hashi];break;}}//没有找到不为空的桶,用nullptr去做end标识if (hashi == _pht->_tables.size()){_node = nullptr;}}return *this;}T& operator*(){return _node->_data;}T* operator->(){return &_node->_data;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}
};

编译器的原则是向上查找(定义必须在前面,否则必须先声明)
4. 封装unordered_map和unordered_set⭕
有了上面的哈希桶的改装,我们这里的对map和set的封装就显得很得心应手了。
unordered_map的封装:
template<class K, class V, class HashFunc = DefaultHash<K>>
class unordered_map
{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};
public:typedef typename Bucket::HashTable<K, pair<K, V>, MapKeyOfT, HashFunc>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));return ret.first->second;}private:Bucket::HashTable<K, pair<K, V>, MapKeyOfT, HashFunc> _ht;
};
这里unordered_map中的operator[ ]我们知道其原理之后,模拟实现就非常方便,直接调用插入函数,控制好参数和返回值即可。
对unordered_set的封装:
template<class K, class HashFunc = DefaultHash<K>>
class unordered_set
{//SteKeyOfT是set专用的就用内部类struct SetKeyOfT{const K& operator()(const K& key){return key;}};public:typedef typename Bucket::HashTable<K, K, SetKeyOfT, HashFunc>::iterator iterator;iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}iterator find(const K& key){return _ht.Find(key);}bool erase(const K& key){return _ht.Erase(key);}
private:Bucket::HashTable<K, K, SetKeyOfT, HashFunc> _ht;
};
相关文章:
【C++、数据结构】封装unordered_map和unordered_set(用哈希桶实现)
文章目录📖 前言1. 复用同一个哈希桶⚡1.1 🌀修改后结点的定义1.2 🌀两个容器各自模板参数类型:2. 改造之后的哈希桶⛳3. 哈希桶的迭代器🔥3.1 💥哈希桶的begin()和 end(…...
StratoVirt 的 vCPU 拓扑(SMP)
CPU 拓扑用来表示 CPU 在硬件层面的组合方式,本文主要讲解 CPU 拓扑中的 SMP(Symmetric Multi-Processor,对称多处理器系统)架构,CPU 拓扑还包括其他信息,比如:cache 等,这些部分会在…...
现在直播大部分都是RTMP RTMP VS RTC
一 RTMP 抓了下抖音直播的包,windows端,走的TCP,加密了,估计还是RTMP。 我以为直播带货,都是RTC了。 快手直播也是TCP,地址用了IPV6。 淘宝直播也是。现在大部分直播都是RTMP。 只有视频会议走的RTC。…...
【Unity实战100例】Unity循环UI界面切换卡片功能
目录 编辑 一:制作UI界面 二:代码逻辑 1.定义基础变量...
Monorepo or 物料市场?结合工作实际情况对公司现有前端体系的思考
前言 去年年中基于若依vue前端框架进行了改造,加上后端的配合,我写了一套脚手架和项目中后台模板。中后台模板中包含了许多基础代码,比如登录/注册、路由、权限等等相关功能。这个中后台模板是基于我们实际开发定制的,所以跟通用…...
GEE学习笔记八十八:在自己的APP中使用绘制矢量(上)
在GEE中尤其是自己的APP中调用绘制的矢量图形方法之前没有合适的方法,但是现在可以通过ui.Map.DrawingTools(...)以及ui.Map.GeometryLayer(...)结合来做。具体的API如下图: 在这一篇中我先通过一个简单的例子来展示一下使用这些API后可以实现什么效果&a…...
“笨办法”学Python 3 ——练习 39. 字典,可爱的字典
练习39 源代码 # create a mapping of state to abbreviation #创建一个州与缩写的映射 states {Oregon:OR,Florida:FL,California: CA, New York: NY,Michigan:MI} #创建一个字典,key为州名,value为州缩写#Create a basic set of states and some cit…...
模糊的照片如何修复清晰?
相信有很多人用手机拍照时,觉得拍出来的照片一定是很漂亮的,结果拍了之后,拿出来一看模糊一片,根本看不清是什么。或者是只显示一半另一半模糊一片。而这些精彩瞬间很多时候是无法重拍的。虽然谁也不想拍出的照片出现模糊…...
如何理解session、cookie、token的区别与联系?
session、cookie、token。 相信学过接口的朋友都特别熟悉了。 但是对我一个刚接触接口测试的小白来说,属实有点分不清楚。 下文就是我通过查阅各种资料总结出来的一点理解,不准确的地方还请各位指正。 (文末送洗浴中心流程指南)…...
【MyBatis】| MyBatis分页插件PageHelper
目录 一:MyBatis使⽤PageHelper 1. limit分⻚ 2. PageHelper插件 一:MyBatis使⽤PageHelper 1. limit分⻚ (1)概念: ①页码:pageNum(用户会发送请求,携带页码pageNum给服务器&am…...
Java枚举类详解
一、定义格式 public enum s { 枚举项1,枚举项2,枚举项3; } // 定义一个枚举类,用来表示春,夏,秋,冬这四个固定值 public enum Season {SPRING,SUMMER,AUTUMN,WINTER; } 二、枚举的特点 1、所有枚举类都是Enum的子类 2、我们可以通…...
C语言数组
一.数组定义 数组由数据类型相同的一系列元素组成 如 int main(){ float candy[365]; char code[12]; int states[50]; … } cnady是包含了365个float元素的数组。code是包含了12个char类型的数组。states包含了50个int类型的数组。 二.数组初始化和取值 我们使用花括号包含值&…...
Scala 入门(第一章Scala 环境搭建、插件的安装)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 第 1 章 Scala 入门1.1 概述1.1.1 为什么学习 Scala1.1.2 Scala 发展历史1.1.3 Scala 和 Java 关系1.1.4 Scala 语言特点1.2 Scala 环境搭建1.3 Scala 插件安装1.4 HelloWorl…...
math@多项式@求和式乘法@代数学基本定理
文章目录多项式求和式乘法应用代数学基本定理相关证明高次方程其他关于多项式的参考多项式求和式乘法 S∏j1(∑k1ajk) j∏j1m(∑k1njajk) jS\prod_{j1}\left(\sum\limits_{k1}a_{jk}\right)_{\!\!\!j} \\\prod_{j1}^{m}\left(\sum\limits_{k1}^{n_j}a_{jk}\r…...
Kafka系列之:基于SCRAM和Ranger机制完成动态新增kafka读写账号、赋予账号对指定Topic的读写权限
Kafka系列之:基于SCRAM和Ranger机制完成动态新增kafka读写账号、赋予账号对指定Topic的读写权限 一、需求背景二、添加写用户三、查看用户是否添加到zookeeper中四、查看用户五、赋予用户topic写权限六、生产者配置文件七、ranger给用户权限八、往Topic写数据九、删除添加的用…...
第五十三章 DFS进阶(一)——剪枝优化
第五十四章 DFS进阶(一)——剪枝优化一、什么是剪枝?二、剪枝优化的策略1、优化搜索顺序2、排除等效冗余3、可行性剪枝4、最优性剪枝5、记忆化搜索三、例题1、AcWing 165. 小猫爬山(DFS 剪枝优化)2、AcWing 167. 木棒…...
Java字节码深度知多少?
文章目录1、字节码结构1.1、基本结构1.2、实际观测2、内存表示3、方法调用指令4、invokedynamicEND结语Java真的是长盛不衰,拥有顽强的生命力。其中,字节码机制功不可没。字节码,就像是 Linux 的 ELF。有了它,JVM直接摇身一变&…...
【C++】智能指针(万字详解)
🌈欢迎来到C专栏~~智能指针 (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort目前状态:大三非科班啃C中🌍博客主页:张小姐的猫~江湖背景快上车🚘,握好方向盘跟我有一起打天下嘞!送给自己的一句鸡汤&…...
使用docker配置mysql主从复制
1.新建主服务器容器实例: docker run -p 3307:3306 --name mysql \ -v /docker/mysql/data:/var/lib/mysql \ -v /docker/mysql/conf:/etc/mysql/conf \ -v /docker/mysql/log:/var/log/mysql \ -e MYSQL_ROOT_PASSWORDroot \ -d mysql:5.7 设置容器卷之后…...
v3 异步组件及分包使用
1 app.vue <template> <!-- vue3异步组件必须使用suspense --> <Suspense> <template #default> <!-- 异步组件 --> <SyncVue></SyncVue> </template> <template v-slot:fallback> <!-- 优先显示骨架屏 --> <…...
变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析
一、变量声明设计:let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性,这种设计体现了语言的核心哲学。以下是深度解析: 1.1 设计理念剖析 安全优先原则:默认不可变强制开发者明确声明意图 let x 5; …...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...
JAVA后端开发——多租户
数据隔离是多租户系统中的核心概念,确保一个租户(在这个系统中可能是一个公司或一个独立的客户)的数据对其他租户是不可见的。在 RuoYi 框架(您当前项目所使用的基础框架)中,这通常是通过在数据表中增加一个…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...
视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
基于IDIG-GAN的小样本电机轴承故障诊断
目录 🔍 核心问题 一、IDIG-GAN模型原理 1. 整体架构 2. 核心创新点 (1) 梯度归一化(Gradient Normalization) (2) 判别器梯度间隙正则化(Discriminator Gradient Gap Regularization) (3) 自注意力机制(Self-Attention) 3. 完整损失函数 二…...
