C++ 哈希表、unordered_map 和 unordered_set 的实现

哈希表(Hash Table)是一种常用的数据结构,它能够以接近常数时间复杂度的效率完成插入、删除和查找操作。在现代编程中,标准模板库(STL)提供了 unordered_mapunordered_set,它们都是基于哈希表实现的。在这篇博客中,我们将从零开始,使用 C++ 实现一个通用的哈希表,并基于此实现 unordered_mapunordered_set,同时完成迭代器的设计。

1. 哈希表的基本原理

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素
时,必须要经过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度, 搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数 使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素。
因此当我们插入元素时可以根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放,
而当我们搜索元素时可以 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功。
综上构造出来的表则称作 “哈希表”,相对应的函数就是“哈希函数”
那么现在我们假设一组数据并且将 哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。
按照上述插入会出现什么问题呢?
我们可以发现4的位置发生了重叠,7的余数也是4,这个4就与我们第一个4发生了冲突,这种情况我们就称“   哈希冲突”: 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞
因此, 引起哈希冲突的一个原因可能是: 哈希函数设计不够合理
因此,常用的一些优秀哈希函数比如 直接定址法,除留余数法,数学分析法等等,我们本篇采用除留余数法, 设散列表中允许的 地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数, 按照哈希函数: Hash(key) = key% p(p<=m), 将关键码转换成哈希地址
不同情况要采取不同策略的哈希函数,但还有一点, 哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。
因此,我们该如何解决哈希冲突呢?
解决哈希冲突 两种常见的方法是: 闭散列 开散列

2. 开放地址法(闭散列)哈希表的实现

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把 key 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置
呢?

1. 线性探测 

从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止

具体实现如下:

C-/unordered_map_set at c0fcac61b3e7110c8e5f3a360eca0d920acf3ccb · hqxnb666/C- · GitHub

首先,我们定义哈希表的状态枚举和数据节点结构

enum State
{EMPTY,   // 空状态EXIST,   // 已存在DELETE   // 被删除
};template<class K, class V>
struct HashData
{std::pair<K, V> _kv;  // 键值对State _state;         // 当前节点状态HashData() : _state(EMPTY) {}
};

接下来,定义哈希函数模板:

template<class K>
struct HashFunc
{size_t operator()(const K& key){return static_cast<size_t>(key);}
};// 针对 std::string 的特化
template<>
struct HashFunc<std::string>
{size_t operator()(const std::string& key){size_t hash = 0;for (char ch : key){hash = hash * 131 + ch;  // 使用 BKDR 哈希算法}return hash;}
};

这个模板是用来干什么的呢?,当我们传递参数时,能只传递整数吗?传递字符串又该如何处理?

因此我们采用了仿函数的方式,重载对应的(),当传递的是字符串时,编译器按需实例化,通过这个BKDR函数来将字符串进行处理后再进行我们的去留余数法,而这个算法的作用是什么呢,我们当前代码影响效率的因素是什么?最大程度上的影响因素是如果处理后的数字一旦发生冲突,就要挨家挨户往后找空,这样很浪费时间,于是我们采取了目前最优秀的字符串处理算法,这样经过处理后的数字很少会发生冲突,最大程度上提高了我们的效率!

以下是哈希表的类定义: 

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:HashTable(size_t size = 10) : _tables(size), _n(0) {}bool Insert(const std::pair<K, V>& kv){// 检查装载因子,决定是否需要扩容if (_n * 10 / _tables.size() >= 7){// 扩容并重新哈希Rehash();}Hash hs;size_t idx = hs(kv.first) % _tables.size();// 线性探测寻找空位while (_tables[idx]._state == EXIST){// 如果键已存在,更新值if (_tables[idx]._kv.first == kv.first){_tables[idx]._kv.second = kv.second;return true;}idx = (idx + 1) % _tables.size();}_tables[idx]._kv = kv;_tables[idx]._state = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash hs;size_t idx = hs(key) % _tables.size();size_t origin = idx;  // 记录初始位置while (_tables[idx]._state != EMPTY){if (_tables[idx]._state == EXIST && _tables[idx]._kv.first == key){return &_tables[idx];}idx = (idx + 1) % _tables.size();if (idx == origin) break;  // 回到起点,查找结束}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret == nullptr) {return false;}else{ret->_state = DELETE;--_n;return true;}}private:void Rehash(){size_t newSize = _tables.size() * 2;std::vector<HashData<K, V>> newTables(newSize);for (auto& data : _tables){if (data._state == EXIST){Hash hs;size_t idx = hs(data._kv.first) % newSize;while (newTables[idx]._state == EXIST){idx = (idx + 1) % newSize;}newTables[idx] = data;}}_tables.swap(newTables);}std::vector<HashData<K, V>> _tables;size_t _n;  // 有效元素个数
};

 这里涉及到一个概念,我们一开始假设的是十个空间大小对应余数,随着数字越来越多,十个一旦不够用了我们就需要对其进行扩容操作,那么什么时候选择扩容比较好呢?太小了浪费空间,太大了效率不够,因此,通过测试目前最适用的负载因子是0.7,

 

 3. 拉链法(开散列)哈希表的实现

拉链法通过数组加链表的方式处理哈希冲突。每个数组元素(桶)存储一个链表,所有映射到同一桶的元素都链接在这个链表中。 

 

定义哈希节点:

template<class T>
struct HashNode
{T _data;HashNode<T>* _next;HashNode(const T& data): _data(data), _next(nullptr){}
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
public:typedef __HTIterator<K, T, KeyOfT, Hash> iterator;typedef HashNode<T> Node;HashTable(size_t size = 10): _tables(size, nullptr), _n(0){}~HashTable(){Clear();}std::pair<iterator, bool> Insert(const T& data){KeyOfT kot;Hash hs;size_t idx = hs(kot(data)) % _tables.size();Node* cur = _tables[idx];while (cur){if (kot(cur->_data) == kot(data)){return { iterator(cur, this), false };  // 元素已存在}cur = cur->_next;}// 插入新节点Node* newNode = new Node(data);newNode->_next = _tables[idx];_tables[idx] = newNode;++_n;// 检查装载因子,决定是否需要扩容if (_n > _tables.size()){Rehash();}return { iterator(newNode, this), true };}bool Erase(const K& key){KeyOfT kot;Hash hs;size_t idx = hs(key) % _tables.size();Node* cur = _tables[idx];Node* prev = nullptr;while (cur){if (kot(cur->_data) == key){if (prev){prev->_next = cur->_next;}else{_tables[idx] = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}iterator Find(const K& key){KeyOfT kot;Hash hs;size_t idx = hs(key) % _tables.size();Node* cur = _tables[idx];while (cur){if (kot(cur->_data) == key){return iterator(cur, this);}cur = cur->_next;}return end();}iterator begin(){for (size_t i = 0; i < _tables.size(); ++i){if (_tables[i]){return iterator(_tables[i], this);}}return end();}iterator end(){return iterator(nullptr, this);}private:void Rehash(){size_t newSize = _tables.size() * 2;std::vector<Node*> newTables(newSize, nullptr);KeyOfT kot;Hash hs;for (auto& head : _tables){Node* cur = head;while (cur){Node* next = cur->_next;size_t idx = hs(kot(cur->_data)) % newSize;cur->_next = newTables[idx];newTables[idx] = cur;cur = next;}}_tables.swap(newTables);}void Clear(){for (auto& head : _tables){Node* cur = head;while (cur){Node* next = cur->_next;delete cur;cur = next;}head = nullptr;}_n = 0;}std::vector<Node*> _tables;size_t _n;  // 有效元素个数
};

 

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,
再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可
以给哈希表增容。
void _CheckCapacity()
{size_t bucketCount = BucketCount();if(_size == bucketCount){HashBucket<V, HF> newHt(bucketCount);for(size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx){PNode pCur = _ht[bucketIdx];while(pCur){// 将该节点从原哈希表中拆出来_ht[bucketIdx] = pCur->_pNext;// 将该节点插入到新哈希表中size_t bucketNo = newHt.HashFunc(pCur->_data);pCur->_pNext = newHt._ht[bucketNo];newHt._ht[bucketNo] = pCur;pCur = _ht[bucketIdx];}}newHt._size = _size;this->Swap(newHt);}
}

4. 基于哈希表实现 unordered_mapunordered_set

 unordered_map 是一种以键值对(key-value)形式存储数据的关联容器,其中每个键都是唯一的。由于哈希表天然支持根据键快速查找对应的值,因此我们可以使用哈希表作为 unordered_map 的底层数据结构。

  • 键值对的存储:需要存储键和值的关联关系,通常使用 std::pair<const K, V>
  • 哈希函数:需要一个能够将键映射到哈希表索引的哈希函数。
  • 键的提取器(Key Extractor):在哈希表中,我们需要从存储的元素中提取键,以进行哈希计算和比较。
  • 迭代器的支持:为了使容器的使用更加方便,我们需要实现迭代器,支持遍历所有元素。

首先,我们定义一个提取键的仿函数 MapKeyOfT,用于从键值对中提取键:

template<class K, class V>
struct MapKeyOfT
{const K& operator()(const std::pair<const K, V>& data) const{return data.first;}
};

接下来,定义 unordered_map 类模板: 

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
public:typedef std::pair<const K, V> ValueType;typedef HashTable<K, ValueType, MapKeyOfT<K, V>, Hash> HashTableType;typedef typename HashTableType::iterator iterator;unordered_map() : _ht() {}// 插入元素std::pair<iterator, bool> insert(const ValueType& kv){return _ht.Insert(kv);}// 查找元素iterator find(const K& key){return _ht.Find(key);}// 访问元素,如果键不存在,则插入默认值V& operator[](const K& key){auto it = _ht.Find(key);if (it == _ht.end()){auto ret = _ht.Insert({ key, V() });return ret.first->second;}else{return it->second;}}// 删除元素bool erase(const K& key){return _ht.Erase(key);}// 迭代器支持iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}private:HashTableType _ht;
};

 

 

  • ValueType 定义ValueType 是存储在哈希表中的数据类型,即键值对 std::pair<const K, V>

  • HashTableType 定义HashTableType 是我们之前实现的哈希表类型,模板参数包括键类型、存储的数据类型、键提取器和哈希函数。

  • insert 函数:调用底层哈希表的 Insert 方法,将键值对插入哈希表。返回值是一个包含迭代器和布尔值的 std::pair,表示插入操作的结果。

  • find 函数:调用底层哈希表的 Find 方法,根据键查找元素,返回迭代器。

  • operator[]:提供数组下标操作符,方便用户访问或修改元素。如果键不存在,则插入一个默认值(值的默认构造),并返回其引用。

  • erase 函数:调用哈希表的 Erase 方法,根据键删除元素。

  • 迭代器支持:通过底层哈希表的迭代器,提供 begin()end() 方法,支持遍历。

unordered_set 的实现 

unordered_set 是一种存储唯一元素的无序容器,不存储键值对。由于只需要存储键,因此可以直接使用哈希表,将键作为存储的元素。

在设计 unordered_set 时,需要考虑:

  1. 元素的存储:直接存储键,类型为 K
  2. 哈希函数:用于将键映射到哈希表索引。
  3. 键的提取器:由于存储的就是键本身,因此键提取器可以直接返回元素本身。
  4. 迭代器的支持:需要提供迭代器,支持遍历所有元素。

 

定义键提取器 SetKeyOfT

template<class K>
struct SetKeyOfT
{const K& operator()(const K& key) const{return key;}
};

 定义 unordered_set 类模板:

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
public:typedef K ValueType;typedef HashTable<K, ValueType, SetKeyOfT<K>, Hash> HashTableType;typedef typename HashTableType::iterator iterator;unordered_set() : _ht() {}// 插入元素std::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);}// 迭代器支持iterator begin(){return _ht.begin();}iterator end(){return _ht.end();}private:HashTableType _ht;
};
  • ValueType 定义:存储的元素类型就是键类型 K

  • HashTableType 定义:使用哈希表,模板参数包括键类型、存储的数据类型(也是 K)、键提取器和哈希函数。

  • insert 函数:调用哈希表的 Insert 方法,将键插入哈希表。

  • find 函数:根据键查找元素,返回迭代器。

  • erase 函数:根据键删除元素。

  • 迭代器支持:提供 begin()end() 方法,支持遍历。

为了使 unordered_mapunordered_set 能够像标准容器一样使用迭代器遍历,我们需要实现符合 STL 标准的迭代器。在哈希表的实现中,我们已经定义了迭代器 __HTIterator,它能够遍历哈希表中的所有元素。unordered_mapunordered_set 的迭代器可以直接使用哈希表的迭代器。

typedef typename HashTableType::iterator iterator;

 通过以上的实现,我们成功地基于哈希表构建了 unordered_mapunordered_set,并提供了迭代器支持,使其用法与标准库中的容器类似。在这个过程中,我们深入理解了哈希表的工作原理,以及如何利用模板和仿函数实现通用的、可扩展的容器。

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

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

相关文章

多行为推荐-KBS 24|基于HyperGRU对比网络的短视频推荐多行为序列建模

论文&#xff1a;https://www.sciencedirect.com/science/article/abs/pii/S0950705124004751?via%3Dihub 关键词&#xff1a;短视频推荐&#xff0c;多行为推荐&#xff0c;对比学习&#xff0c;RNN 1 动机 这是我第一次看短视频推荐里涉及到多行为的论文&#xff0c;动机还…

【AI系统】ESPNet 系列

ESPNet 系列 本文将会介绍 ESPNet 系列&#xff0c;该网络主要应用在高分辨率图像下的语义分割&#xff0c;在计算内存占用、功耗方面都非常高效&#xff0c;重点介绍一种高效的空间金字塔卷积模块&#xff08;ESP Module&#xff09;&#xff1b;而在 ESPNet V2 上则是会更进…

基于vpk180边缘场景下分布式神经网络训练模型部署

本项目目标在于针对边缘场景实现P2P的分布式训练过程&#xff0c;设计方案为将神经网络训练过程对应的裸机程序部署在了PS端的ARM Cortex-A72核上&#xff0c;传输方案采用开发板板载的GTM收发器硬件资源通过外部QSFP-DD光模块光传输至对端&#xff0c;最终完成了设计目标。 整…

ros项目dual_arm_pick-place(urdf文件可视化查看)

前言 一直想写一些项目的讲解&#xff0c;今天&#xff08;2024.12.05&#xff09;可以说正式开始了。 dual_arm_pick-place项目&#xff0c;是关于两个机械臂协同传递物品。 正文 这次的话&#xff0c;给大家讲一下里面的urdf文件。 这篇文章主要来看一下项目中的urdf文件…

spring boot 配置文件加载的加载和使用

文章目录 添加配置信息使用ConfigurationProperties注入属性使用Value注入属性两种方式对比分析自定义配置添加自定义配置加载并使用配置信息 添加配置信息 在实际开发中&#xff0c;应用程序的配置文件可能会变得非常复杂&#xff0c;属性的数量也会很多。为了更好地组织和管…

17. Threejs案例-Three.js创建多个立方体

17. Threejs案例-Three.js创建多个立方体 实现效果 知识点 WebGLRenderer (WebGL渲染器) WebGLRenderer 是 Three.js 中用于渲染 WebGL 场景的核心类。它负责将场景中的对象渲染到画布上。 构造器 new THREE.WebGLRenderer(parameters) 参数类型描述parametersObject可选…

QNX的内存布局和启动入口

参考资料: QNX官网文档 内存布局 添加图片注释,不超过 140 字(可选) 查看系统内存布局 # pidin syspage=asinfo Header size=0x00000108, Total Size=0x0000d1b0, #Cpu=8, Type=257 Section:asinfo offset:0x0000bdf0 size:0x00000d00 elsize:0x000000200000

【成功解决】:VS2019(Visual Studio 2019)遇到E2870问题:此配置中不支持 128 位浮点类型

起因:项目中需要用json来操作数据,就引了cJSON库(cJSON.h和cJSON.c文件),但是发现编译报错如下 E2870 此配置中不支持 128 位浮点类型 test0 ...\usr\include\x86_64-linux-gnu\bits\floatn.h 75 然后先新建了个工程来检查问题(甚至在这之前还以为是cjson…

【LeetCode】122.买卖股票的最佳时机II

文章目录 题目链接&#xff1a;题目描述&#xff1a;解题思路一&#xff08;贪心算法&#xff09;&#xff1a;解体思路二&#xff08;动态规划&#xff09;&#xff1a; 题目链接&#xff1a; 122.买卖股票的最佳时机II 题目描述&#xff1a; 解题思路一&#xff08;贪心算法…

【源码解读】SpringMMVC执行流程

直接进入主题&#xff0c;当我们执行一条请求的时候&#xff0c;是会进入到org.springframework.web.servlet.DispatcherServlet类的doDispatch方法中 这个方法是从HandleMapping中获取对应的Handle 其实就是得到我们需要执行的某个方法 接下来判断这个mapperhandle是否为空 如…

【时时三省】(C语言基础)结构体的变量定义和初始化

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 有了结构体类型&#xff0c;那如果定义变量&#xff0c;其实很简单。 示例&#xff1a; 这个就是结构体变量的基础创建 初始化 比如里面只剩一个s3 s3里面有两个成员 第一个给c的值 第二个给…

与火山引擎合作深化,观测云携一站式监控解决方案登陆万有商城

近日&#xff0c;观测云正式宣布入驻火山引擎的万有商城。作为一款全栈式数据观测与分析平台&#xff0c;观测云的加入不仅丰富了火山引擎生态&#xff0c;也为广大企业用户带来了更便捷的数字化工具&#xff0c;助力企业快速实现业务监控与优化。 从全球覆盖到本地深耕&#x…

【2025最新计算机毕业设计】基于SpringBoot+Vue旅游路线推荐系统【提供源码+答辩PPT+文档+项目部署】

作者简介&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流。✌ 主要内容&#xff1a;&#x1f31f;Java项目、Python项目、前端项目、PHP、ASP.NET、人工智能…

Docker安装Valkey教程(图文教程)

本章教程,介绍如何使用Docker启动Valkey的具体步骤。 一、什么是Valkey? Valkey 是一个高性能的开源键值存储系统,支持多种工作负载,包括缓存、消息队列和主数据库。它可以作为独立守护进程或集群运行,并提供复制和高可用性功能。 Valkey 原生支持多种数据类型,如字符串、…

漫画之家系统:Spring Boot技术下的漫画社区构建

3 系统分析 3.1系统可行性分析 3.1.1经济可行性 由于本系统是作为毕业设计系统&#xff0c;且系统本身存在一些技术层面的缺陷&#xff0c;并不能直接用于商业用途&#xff0c;只想要通过该系统的开发提高自身学术水平&#xff0c;不需要特定服务器等额外花费。所有创造及工作过…

10a大电流稳压芯片_24v转3.3v稳压芯片,高效率DC-DC变换器10A输出电流芯片-AH1514

### AH1514——高性能的大电流稳压芯片 在现代电子电路设计中&#xff0c;对于能够满足大电流、高效率转换以及稳定电压输出的芯片需求日益增长。AH1514芯片作为一款出色的DC-DC变换器&#xff0c;以其独特的性能特点&#xff0c;在众多应用场景中展现出了卓越的优势. ### 一…

三轴云台之数字变倍功能篇

一、数字变倍的定义 数字变倍是指通过电子方式对图像进行放大或缩小的过程。与光学变焦不同&#xff0c;数字变焦不会改变镜头的焦距&#xff0c;而是通过图像处理算法对图像进行放大或缩小&#xff0c;从而改变图像的视野范围。 二、三轴云台数字变倍的特点 灵活性&#xff…

K8S的ingress介绍和安装ingress

1 ingress介绍 1.1 ingress架构图 1.2 ingress相关概念 ingress诞生背景&#xff1a; 在没有ingress之前&#xff0c;只能基于svc的NodePort或者LoadBalancer实现内部的pod对外访问&#xff0c;如果遇到多个服务要监听80端口时。 很明显无论哪种类型都无法实现&#xff0c;如…

无公网IP实现飞牛云手机APP远程连接飞牛云NAS管理传输文件

文章目录 前言1. 本地连接测试2. 飞牛云安装Cpolar3. 配置公网连接地址4. 飞牛云APP连接测试5. 固定APP远程地址6. 固定APP地址测试 前言 随着科技的不断发展&#xff0c;远程访问和控制内网设备的需求日益增加。在众多内网设备中&#xff0c;网络附加存储&#xff08;NAS&…

第一篇:k8s架构与组件详解

没有那么多花里胡哨&#xff0c;直接进行一个K8s架构与组件的学习。 一、K8s架构 在Master通常上包括 kube-apiserver、etcd 存储、kube-controller-manager、cloud-controller-manager、kube-scheduler 和用于 K8s 服务的 DNS 服务器&#xff08;插件&#xff09;。这些对集群…