当前位置: 首页 > news >正文

【C++ —— 认识哈希和unordered_set、unordered_map的介绍及模拟】

认识哈希和unordered_set、unordered_map的介绍及模拟

  • 哈希表基础
    • 哈希的概念
    • 哈希表的基本操作
  • 哈希冲突
    • 哈希冲突的定义
    • 哈希冲突的影响
    • 常见的哈希冲突的解决方法
  • 哈希函数
    • 哈希函数的定义
    • 哈希函数的设计原则
    • 常见的哈希函数
  • unordered系列关联式容器
  • hash模拟

哈希表基础

哈希的概念

哈希表(Hash Table),也称为散列表,是一种通过哈希函数将键值(Key) 映射到对应位置(Bucket)数据结构。哈希表使用一个数组来存储数据,通过哈希函数计算出键值的索引,插入、查找和删除操作的时间复杂度可以接近 O ( 1 ) O(1) O(1)

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

哈希表的基本操作

当向哈希表的结构中:

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
    取元素比较,若关键码相等,则搜索成功。

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(HashTable)(或者称散列表)


例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

在这里插入图片描述
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?

哈希冲突

哈希冲突的定义

哈希冲突(Hash Collision) 是指两个或多个不同的键通过哈希函数映射到同一个位置。当哈希表发生冲突时,需要使用某种机制来解决冲突,确保所有键值对能够正确存储和查找。

哈希冲突的影响

哈希冲突会导致数据存储位置重叠,增加查找时间,降低哈希表的整体性能。频繁的冲突会使哈希表的操作复杂度从 O ( 1 ) O(1) O(1)恶化为 O ( n ) O(n) O(n),影响系统性能。

常见的哈希冲突的解决方法

1. 开放地址法
开放地址法通过探测空闲位置来解决冲突。常见的探测方法有:

下面讲重点讲解线性探测二次探测分离链接法

线性探测(Linear Probing)

  • 发生冲突时,从当前位置开始,逐个检查后续位置,直到找到空位为止。
  • 优点:实现简单。
  • 缺点:可能导致“堆积”现象,即冲突连续发生。

比如上述中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

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

  • 插入

    • 通过哈希函数获取待插入元素在哈希表中的位置
    • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
      在这里插入图片描述
  • 删除

    • 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索素。 比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。

二次探测(Quadratic Probing)

  • 发生冲突时,从当前位置开始,以二次方为步长逐个检查后续位置(例如,1^2, 2^2, 3^2,…),直到找到空位为止。
  • 优点:减少线性探测中的堆积现象。
  • 缺点:需要处理哈希表大小和步长选择的问题。

二次探测

 线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为: H i H_i Hi =( H 0 H_0 H0 + i 2 i^2 i2 )% m, 或者: H i H_i Hi = ( H O H_O HO - i 2 i^2 i2 )% m。其中: i =1,2,3., H o H_o Ho是通过散列函数Hash(x)对元素的关键码key进行计算得到的位置,m是表的大小。对于上述中如果要插入44,产生冲突,使用解决后的情况为:

在这里插入图片描述

  研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜素时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。


双重哈希(Double Hashing)

  • 发生冲突时,使用第二个哈希函数计算新的步长,从当前位置开始探测空闲位置。
  • 优点:进一步减少冲突聚集。
  • 缺点:需要设计两个有效的哈希函数。

2. 链地址法
链地址法通过在每个哈希表位置存储一个链表来解决冲突。

分离链接(Separate Chaining)

  • 分离链接法通过将每个哈希表槽位维护一个链表来处理冲突。
  • 每个哈希表槽位存储一个链表,所有冲突的元素都插入该链表中。
  • 优点:简单易实现,动态调整链表长度。
  • 缺点:在最坏情况下(所有元素都落在同一个桶中),查找时间复杂度退化为 O ( n ) O(n) O(n)

在这里插入图片描述
在这里插入图片描述

自适应链表(Adaptive Chaining)

  • 根据链表长度和冲突情况,动态调整存储结构,例如在链表长度较长时将其转换为红黑树。
  • 优点:在链表较短时保持简单性,在链表较长时提高查找效率。
  • 缺点:实现较为复杂,需要维护多种数据结构。

哈希函数

哈希函数的定义

哈希函数的定义
 哈希函数(Hash Function)是将键值转换为哈希值的函数。哈希值用于确定键值在哈希表中的存储位置。一个好的哈希函数能均匀分布键,减少冲突,提高哈希表的性能。

哈希函数的设计原则

  • 均匀分布:哈希值应均匀分布在整个哈希表,避免冲突集中在某些位置。
  • 快速计算:哈希函数应高效计算,减少额外开销,提高性能。
  • 最小冲突:尽量避免相同哈希值的情况,使不同的键尽量映射到不同的位置。

常见的哈希函数

1. 除留余数法 (Modulo Division Method)
 解释:除留余数法是最简单和常用的哈希函数之一。它通过将键除以哈希表的大小并取余数来生成哈希值。这样生成的哈希值会在0到表大小-1的范围内,适合直接作为哈希表的索引。

优点:
 实现简单,计算快速。
缺点:
 如果键的分布不均匀,可能导致冲突较多。
C++代码

int modHash(int key, int table_size) 
{return key % table_size;
}

2. 平方取中法 (Mid-Square Method)
 解释:平方取中法是将键值平方,然后取中间几位作为哈希值。这样做的优点是可以通过平方操作增加哈希值的复杂性,从而使得哈希值更均匀地分布。

优点:
 中间位数的选择可以减少键值的偏差,减少冲突。
缺点:
 适用于键值长度较短的情况。
C++代码:

int midSquareHash(int key, int table_size) 
{int squared = key * key;// 假设key是一个4位数,取中间2位int mid_digits = (squared / 100) % 100;return mid_digits % table_size;
}

3. 直接定址法 (Direct Addressing Method)
 解释:直接定址法将键值直接映射到哈希表的索引上,适用于键值范围较小且较为密集的情况。键值直接作为索引使用,不需要额外计算。

优点:
 查找、插入和删除操作的时间复杂度都是O(1)。
 实现非常简单。
缺点:
 需要保证键值的范围在哈希表大小之内,否则会导致索引越界。
 键值范围大且稀疏时会浪费内存。
C++代码:

int multiplicativeHash(int key, int table_size) 
{double A = 0.618033;  // (sqrt(5) - 1) / 2 的值double frac = key * A - int(key * A);return int(table_size * frac);
}

unordered系列关联式容器

unordered系列关联式容器 包含 unordered_mapunordered_set,在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同。

unordered_map/unordered_set 和 map/set 的区别

特性map/setunordered_map/unordered_set
内部实现红黑树(自平衡二叉搜索树)哈希表
元素顺序有序(按键值排序)无序(按哈希值存储)
时间复杂度O(log n)平均 O(1),最坏 O(n)
迭代顺序按键值顺序无固定顺序
内存占用较高(存储节点信息)较低(可能有冲突链表开销)
使用场景需要有序存储和范围查询需要快速查找、插入和删除

简要总结

  • map 和 set:适用于需要保持元素有序的场景,例如有序字典、按键值范围查询。
  • unordered_map 和 unordered_set:适用于需要快速查找、插入和删除的场景,且对元素顺序没有要求。

unordered_map/unordered_set 的接口可以查询cplusplus.com使用用法与之前介绍的相似。

hash模拟

开放地址法hash

#pragma once
#include <vector>template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};//特化
template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto ch : key){hash *= 131;hash += ch;}return hash;}
};namespace open_address
{enum State{EMPTY,EXIST,DELETE};template<class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY;};struct StringHashFunc{size_t operator()(const string& k){size_t hash = 0;for (auto& e : k){hash *= 131;hash += e;}return hash;}};template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable(){_tables.resize(10);}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//扩容if (_n * 10 / _tables.size() >= 7){HashTable<K, V, Hash> newHT;newHT._tables.reserve(_tables.size() * 2);//旧表插入到新表for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST){newHT.Insert(_tables[i]._kv);}}_tables.swap(newHT._tables);}Hash hs;size_t hashi = hs(kv.first) % _tables.size();//线性探测while (_tables[hashi]._state == EXIST){hashi++;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST;_n++;return true;}HashData<K, V>* Find(const K& k){Hash hs;size_t hashi = hs(k) % _tables.size();//线性探测while (_tables[hashi]._state != EMPTY){if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == k){return &_tables[hashi];}hashi++;hashi %= _tables.size();}return nullptr;}bool Erase(const K& k){HashData<K, V>* ret = Find(k);if (ret == nullptr){return false;}else{ret->_state = DELETE;_n--;return true;}}private:vector<HashData<K, V>> _tables;size_t _n = 0;		//有效数据个数};

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【学习笔记】Redis学习笔记——第17章 集群
  • Mojo简介
  • 打卡第22天------回溯算法
  • 深度学习系列70:模型部署torchserve
  • python 裁剪图片
  • 《梁宁产品思维30讲》是一门深入剖析产品思维、产品认知框架的课程
  • Windows11和Win10如何彻底永久关闭Windows defender
  • MySQL可重复读的隔离机制下是否彻底解决了幻读?
  • 云服务部署项目(Spring + Vue)
  • vue-router小结
  • Python3网络爬虫开发实战(1)爬虫基础
  • Vue.js 与 Ajax(vue-resource)的集成应用
  • Vue 项目部署后首页白屏问题排查与解决
  • WEBKIT 通过JavaScript 调用本地,硬件未来之窗OS硬件APP
  • 03、爬虫数据解析-bs4解析/xpath解析
  • 收藏网友的 源程序下载网
  • [分享]iOS开发 - 实现UITableView Plain SectionView和table不停留一起滑动
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • CAP 一致性协议及应用解析
  • gf框架之分页模块(五) - 自定义分页
  • Java 网络编程(2):UDP 的使用
  • Java 最常见的 200+ 面试题:面试必备
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • Javascript设计模式学习之Observer(观察者)模式
  • js ES6 求数组的交集,并集,还有差集
  • mysql 5.6 原生Online DDL解析
  • Node 版本管理
  • 爱情 北京女病人
  • 创建一种深思熟虑的文化
  • 将回调地狱按在地上摩擦的Promise
  • 模仿 Go Sort 排序接口实现的自定义排序
  • 前端相关框架总和
  • 延迟脚本的方式
  • 正则表达式
  • 好程序员web前端教程分享CSS不同元素margin的计算 ...
  • ​​​​​​​Installing ROS on the Raspberry Pi
  • ​批处理文件中的errorlevel用法
  • ​软考-高级-信息系统项目管理师教程 第四版【第19章-配置与变更管理-思维导图】​
  • # AI产品经理的自我修养:既懂用户,更懂技术!
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • (175)FPGA门控时钟技术
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (3) cmake编译多个cpp文件
  • (3)Dubbo启动时qos-server can not bind localhost22222错误解决
  • (4)事件处理——(7)简单事件(Simple events)
  • (顶刊)一个基于分类代理模型的超多目标优化算法
  • (附源码)spring boot校园健康监测管理系统 毕业设计 151047
  • (附源码)ssm教材管理系统 毕业设计 011229
  • (附源码)ssm旅游企业财务管理系统 毕业设计 102100
  • (切换多语言)vantUI+vue-i18n进行国际化配置及新增没有的语言包
  • (十三)Flask之特殊装饰器详解
  • (十三)MipMap
  • (已解决)vue+element-ui实现个人中心,仿照原神
  • (原創) 如何優化ThinkPad X61開機速度? (NB) (ThinkPad) (X61) (OS) (Windows)