【C++练级之路】【Lv.18】哈希表(哈希映射,光速查找的魔法)
文章目录
- 引言
- 一、哈希
- 1.1 哈希概念
- 1.2 哈希函数
- 1.3 哈希冲突
- 二、闭散列
- 2.1 数据类型
- 2.2 成员变量
- 2.3 默认成员函数
- 2.3.1 constructor
- 2.4 查找
- 2.5 插入
- 2.6 删除
- 三、开散列
- 3.1 结点
- 3.2 成员变量
- 3.3 默认成员函数
- 3.3.1 constructor
- 3.3.2 destructor
- 3.4 查找
- 3.5 插入
- 3.6 删除
- 3.7 哈希化
- 总结
引言
之前学习的红黑树,增删查改都为O(logN),但是今天学习的哈希表,理论上可以达到增删查改都为O(1),让我们来看看是什么结构这么神奇吧~
一、哈希
1.1 哈希概念
在线性结构和树形结构中,元素键值key与其存储位置之间没有对应关系,因此在查找指定元素时,要经过key的多次对比。
时间复杂度:顺序查找为O(N),二叉搜索平衡树查找为O(logN)。
理想的查找方式:不经过任何比较,直接通过key获取其存储位置。
这就是哈希的本质,通过某种函数(称之为哈希函数)构建key与其存储位置的一一映射关系,从而达到查找为O(1)。而这种结构也称为哈希表(Hash Table),又称散列表。
1.2 哈希函数
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部key,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
那么,下面介绍两种常见的哈希函数:
- 直接定址法
- Hash(key) = A*key + B
优点:简单、均匀
缺点:需要事先知道key的分布情况
- 除留余数法
- Hash(key) = key % p (p<=m)
- 其中m为地址数,p为最接近m的素数
优点:不需要事先知道key的分布情况
缺点:会产生哈希冲突
选择除数为素数的原因:减少哈希冲突
如果选择的除数包含多个正因数,那么哈希地址可能会集中在某些特定的值上,从而导致冲突概率增加。
1.3 哈希冲突
哈希冲突,又称哈希碰撞,即为不同key通过相同哈希函数计算出相同的哈希地址。
数学表达:对于两个数据元素的关键字 k i k_i ki和 k j k_j kj(i != j),有 k i k_i ki != k j k_j kj,有:Hash( k i k_i ki) == Hash( k j k_j kj)
面对陌生数据,我们一般比较常用的除留余数法会产生哈希冲突,而哈希冲突则是影响哈希表效率的关键因素。
那么,如何解决哈希冲突呢?这里有两种方法:闭散列和开散列
二、闭散列
闭散列,又称开放定址法。
当哈希冲突发生时,开放定址法尝试在哈希表内部找到一个空闲的单元来存放冲突的元素。这个空闲的单元被称为开放单元或空白单元。
2.1 数据类型
enum State
{EMPTY,EXIST,DELETE
};template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};
细节:
- 每个哈希数据,都要设置状态变量,以便区分
- 状态分为空,存在和删除,数据状态初始化为空
2.2 成员变量
template<class K, class V>
class HashTable
{
public:
protected:vector<HashData<K, V>> _tables;size_t _n = 0;//有效数据个数
};
细节:
- 哈希表底层一般使用数组(vector)
- 哈希表的有效数据个数_n与vector的size不同
2.3 默认成员函数
2.3.1 constructor
HashTable()
{_tables.resize(10);
}
细节:这里vector提前开空间,可以避免后续为空的讨论
2.4 查找
HashData<K, V>* Find(const K& key)
{size_t hashi = key % _tables.size();size_t pos = hashi;size_t i = 1;while (_tables[pos]._state != EMPTY){if (_tables[pos]._state == EXIST && _tables[pos]._kv.first == key){return &_tables[pos];}pos = hashi + i;if (pos >= _tables.size()){return nullptr;}++i;}return nullptr;
}
细节:
- 先用key取模数组size,得到哈希地址hashi
- 然后沿当前位置向后找,直到该位置状态为空或超出数组边界,才算找不到
- 如果该位置状态为存在且key相等,则找到了
2.5 插入
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))//保持key唯一{return false;}//...size_t hashi = kv.first % _tables.size();size_t pos = hashi;size_t i = 1;while (_tables[pos]._state == EXIST){pos = hashi + i;//线性探测if (pos >= _tables.size()){return false;}++i;}_tables[pos]._kv = kv;_tables[pos]._state = EXIST;++_n;return true;
}
细节:
- 先查找当前是否存在该值,如果存在,则不插入
- 用key取模数组size,得到哈希地址hashi
- 然后沿当前位置向后找,直到状态为空或删除,才插入
但是,上述情况是哈希表未满时,如果满了如何扩容?还有,一定要满了才扩容吗?
这里,我们引入负载因子的概念:α = 有效数据个数 / 哈希表长度
当负载因子越大,哈希冲突的概率就越大,同时发生哈希踩踏的概率也越大,对于开放定址法,应该控制负载因子小于0.7,超过将扩容。
if (_n * 10 / _tables.size() >= 7)//负载因子大于等于0.7, 扩容
{size_t newsize = _tables.size() * 2;vector<HashData<K, V>> newtables(newsize);for (auto& cur : _tables){size_t hashi = cur._kv.first % newsize;size_t pos = hashi;size_t i = 1;while (newtables[pos]._state == EXIST){pos = hashi + i;//线性探测++i;}newtables[pos]._kv = kv;_tables[pos]._state = EXIST;}_tables.swap(newtables);
}
细节:
- 判断时左右同乘以10,避免比较浮点数而带来误差
- newsize为原本的2倍(本来应该是接近2倍的素数,这里简单起见没实现)
- 将原哈希表中的元素一一映射到新表中
- 最后交换旧表和新表(类似于拷贝构造的现代写法)
2.6 删除
bool Erase(const K& key)
{HashData<K, V>* ret = Find(key);if (ret){ret._state = DELETE;--_n;return true;}return false;
}
细节:
- 先查找当前是否存在该值,如果存在,则删除
- 这里的删除,只用将状态变量改为删除即可
以上讲解的查找和插入,它们所用的探测方法是线性探测(一个一个往后找),这种探测方法可能会造成大量的哈希冲突。
那么,有没有什么探测方法能缓解哈希冲突呢?有,那就是二次探测!
改法也很简单,以一小段代码举例:
while (newtables[pos]._state == EXIST)
{pos = hashi + i*i;//二次探测++i;
}
这样就是每次跨越 i 的二次方向后探测,中间间隔大,哈希冲突就可以得到缓解。
三、开散列
但是,闭散列(开放定址法)有一个致命的缺陷,那就是空间利用率低!它必须保留相当一部分的开放空间,才能不断插入。
所以,实际上,我们更常用另一种方式来实现哈希表——闭散列,又称为开链法。
在开链法中,哈希表的每个槽位(bucket),又称为哈希桶,通过一个单链表来存储所有散列到该槽位的元素。这意味着即使不同的key经过哈希函数映射到同一个槽位,它们也可以被存储在同一个单链表上,从而避免了冲突。
3.1 结点
template<class K, class V>
struct HashNode
{HashNode<K, V>* _next;pair<K, V> _kv;HashNode(const pair<K, V>& kv): _next(nullptr), _kv(kv){}
};
细节:
- 这里没有使用STL的list或者forward_list,而是自己设计结点,为了更方便操纵内部细节
3.2 成员变量
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
protected:typedef HashNode<K, V> Node;
public:
protected:vector<Node*> _tables;size_t _n = 0;//有效数据个数
};
细节:
- 数组(vector)中存储单链表的头结点指针
- 模板参数的Hash,是为了任意类型都能转换为整型来取模
3.3 默认成员函数
3.3.1 constructor
HashTable()
{_tables.resize(10);
}
细节:这里vector提前开空间,可以避免后续为空的讨论
3.3.2 destructor
~HashTable()
{for (auto& cur : _tables){while (cur){Node* del = cur;cur = cur->_next;delete del;}}
}
细节:因为涉及链表结点空间的动态开辟,所以要手动释放
3.4 查找
Node* Find(const K& key)
{Hash hash;size_t hashi = hash(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}
细节:
- 先取模计算出哈希地址
- 再沿当前单链表向下查找
3.5 插入
bool Insert(const pair<K, V>& kv)
{if (Find(kv.first))//保持key唯一{return false;}Hash hash;//...size_t hashi = hash(kv.first) % _tables.size();Node* newnode = new Node(kv);//头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}
细节:
- 先查找当前是否存在该值,如果存在,则不插入
- 取模计算出哈希地址,再头插新节点
运用开链法后,虽然没有哈希冲突了,但是链表长度过长也会影响效率。所以,哈希表也需要通过扩容来使链表长度变短,理想的状态是负载因子为1时扩容。
悄悄说一句:链表过长,还有另一种解决方法,那就是在该哈希桶下改挂一棵红黑树~
if (_n == _tables.size())//负载因子为1时,扩容{size_t newsize = _tables.size() * 2;vector<Node*> newtables(newsize);for (auto& cur : _tables){while (cur){Node* next = cur->_next;//将旧表结点重新映射到新表上size_t hashi = hash(cur->_kv.first) % newsize;cur->_next = newtables[hashi];newtables[hashi] = cur;//跳回旧表的下一结点cur = next;}}_tables.swap(newtables);}
细节:
- 二倍扩容(本来应该是接近2倍的素数,这里简单起见没实现)
- 遍历旧表,将旧表结点重新映射到新表上(这里直接链接,而不是创建新节点)
- 最后交换旧表和新表
3.6 删除
bool Erase(const K& key)
{Hash hash;size_t hashi = hash(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;
}
细节:
- 单链表删除,设置prev前置指针
- 注意头删的情况,分类处理
3.7 哈希化
由于除留余数法涉及到取模运算,而只有整型才能取模。所以针对非整型的数据,需要将其转化为整型,这一过程称为哈希化。
template<class K>
struct HashFunc
{size_t operator()(const K& key){return key;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t hash = 0;for (auto& ch : s){hash = hash * 31 + ch;}return hash;}
};
细节:
- 第一个哈希化函数,针对的是内置类型(整型或浮点型等),返回值设置为size_t,相近类型会进行隐式类型转换
- 第二个哈希化函数,针对的是字符串,运用了模板的特化。同时,为了防止字符串的异位串(对应字符数相同,而位置不同),并不是直接相加,而是每次相加后乘以31,保证肯定不重复。
- 同时,如果针对特殊的类,用户可以手写一个特定的哈希化函数进行模板传参
总结
相比闭散列,开散列看似增加了存储指针的空间开销,实际上闭散列要保证大量的空闲单元以降低哈希冲突,所以开散列反而更加节省空间,其空间利用率更高!
哈希表与红黑树的对比:
- 哈希表平均查找可达O(1),但最坏降到O(N)(哈希冲突)
- 红黑树最坏查找也可保持O(logN),比较稳定
数据有序性:哈希表无序,而红黑树有序
适用场景:哈希表适合单点查找,红黑树适合范围查找
相关文章:

【C++练级之路】【Lv.18】哈希表(哈希映射,光速查找的魔法)
快乐的流畅:个人主页 个人专栏:《算法神殿》《数据结构世界》《进击的C》 远方有一堆篝火,在为久候之人燃烧! 文章目录 引言一、哈希1.1 哈希概念1.2 哈希函数1.3 哈希冲突 二、闭散列2.1 数据类型2.2 成员变量2.3 默认成员函数2.…...
「PHP系列」If...Else语句/switch语句
文章目录 一、If...Else语句1. 基本语法2. 带有 elseif 的语法3. 示例示例 1:基本 if...else 结构示例 2:使用 elseif示例 3:嵌套 if...else 结构 4. 注意事项 二、switch语句1. 基本语法2. 示例示例 1:基本 switch 结构示例 2&am…...

Ubuntu部署BOA服务器
BOA服务器概述 BOA是一款非常小巧的Web服务器,源代码开放、性能优秀、支持CGI通用网关接口技术,特别适合用在嵌入式系统中。 BOA服务器主要功能是在互联嵌入式设备之间进行信息交互,达到通用网络对嵌入式设备进行监控,并将反馈信…...
安卓Glide加载失败时点击按钮重新加载图片
需求 假设此时已经用load指定一个url: String,又用into指定了一个img: ImageView开始加载,但是网络突然中断,导致图片加载失败。在这种情况下,想要通过点击一个Button重新加载。 Glide.with(context).load(url).placeholder(loa…...
linux下python服务定时(自)启动
AI应用开发相关目录 本专栏包括AI应用开发相关内容分享,包括不限于AI算法部署实施细节、AI应用后端分析服务相关概念及开发技巧、AI应用后端应用服务相关概念及开发技巧、AI应用前端实现路径及开发技巧 适用于具备一定算法及Python使用基础的人群 AI应用开发流程概…...
awk命令进阶操作(二)
awk模块 awk模块awk的BEGIN模块和END模块BEGIN模块BEGIN 常见错误END模块END模块 常见错误 案例计算1~100的累加和统计系统中有多少用户的shell类型是/bin/bash awk模块 awk的BEGIN模块和END模块 格式 awk BEGIN{}{}END{} 文件名BEGIN模块 用于定义一个动作,用{…...

【洛谷 P8695】[蓝桥杯 2019 国 AC] 轨道炮 题解(映射+模拟+暴力枚举+桶排序)
[蓝桥杯 2019 国 AC] 轨道炮 题目描述 小明在玩一款战争游戏。地图上一共有 N N N 个敌方单位,可以看作 2D 平面上的点。其中第 i i i 个单位在 0 0 0 时刻的位置是 ( X i , Y i ) (X_i, Y_i) (Xi,Yi),方向是 D i D_i Di (上下左右之一, 用…...

高阶DS---AVL树详解(每步配图)
目录 前言: AVL树的概念: AVL树节点的定义: AVL树的插入(重点) AVL树的旋转: (1)新节点插入较高左子树的左侧---右单旋 (2)新节点插入较高右子树的右侧---左单旋 …...

c++前言
目录 1. 什么是 C 2. C 发展史 3. C 的重要性 4. 如何学习 C 5. 关于本门课程 1. 什么是C C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的 程序,需要高度的抽象和建模时, C 语言则不合适…...

2024年泰迪杯数据挖掘B题详细思路代码文章教程
目前b题已全部更新包含详细的代码模型和文章,本文也给出了结果展示和使用模型说明。 同时文章最下方包含详细的视频教学获取方式,手把手保姆级,模型高精度,结果有保障! 分析: 本题待解决问题 目标&#…...

练习 21 Web [GXYCTF2019]BabySQli
SQL联合查询,注意有源码看源码,Base64以及32的区别,MD5碰撞 打开后有登录框,先随意登录尝试 只有输入admin才是返回wrong pass! 其他返回wrong user 所以用户名字段一定要输入admin 养成好习惯,先查看源码…...

【并发编程】CountDownLatch
📝个人主页:五敷有你 🔥系列专栏:并发编程 ⛺️稳中求进,晒太阳 CountDownLatch 概念 CountDownLatch可以使一个获多个线程等待其他线程各自执行完毕后再执行。 CountDownLatch 定义了一个计数器,…...

2024-HW --->SSRF
这不是马上准备就要护网了嘛,如火如荼的报名ing!!!那么小编就来查缺补漏一下以前的web漏洞,也顺便去收录一波poc!!!! 今天讲的主人公呢就是SSRF,以前学的时候…...

该主机与 Cloudera Manager Server 失去联系的时间过长。 该主机未与 Host Monitor 建立联系
该主机与 Cloudera Manager Server 失去联系的时间过长。 该主机未与 Host Monitor 建立联系 这个去集群主机cm界面上看会出现这个错误 排查思路: 一般比较常见的原因可能是出问题的主机和集群主节点的时间对应不上了。还有就是cm agent服务出现问题了 去该主机的…...
【BUG】No module named ‘dnf‘
报错内容: 类型一 # git clone https://github.com/pytorch/vision.git Cloning into vision... /usr/libexec/git-core/git-remote-https: symbol lookup error: /usr/lib64/libldap.so.2: undefined symbol: EVP_md2, version OPENSSL_1_1_0类型二 # yum reins…...
Ubuntu pycharm配置Conda环境
参考博客:https://blog.csdn.net/qq_40726937/article/details/105323965 https://juejin.cn/post/7229543139950051388 Ubuntu20.04中搭建虚拟环境并且用pycharm调用Ubuntu中的虚拟环境。_ubuntu pycharm的虚拟环境选哪个-CSDN博客...
工作体验记录
文章目录 如何提高说话能力?如何提高行动力?如何完成一个任务产出成果?如何寻找突破点提高解决问题的效率?如何成为技术领导?参考资料 如何提高说话能力? 三思而后说,想清楚问题描述,抓住重点…...

YOLO火灾烟雾检测数据集:20000多张,yolo标注完整
YOLO火灾烟雾检测数据集:一共20859张图像,yolo标注完整,部分图像应用增强 适用于CV项目,毕设,科研,实验等 需要此数据集或其他任何数据集请私信...

基于Spring Boot的餐厅点餐系统
基于Spring Boot的餐厅点餐系统 开发语言:Java框架:springbootJDK版本:JDK1.8数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:Maven3.3.9 部分系统展示 管理员登录界面 用户注册登录界面 …...
tkinter控件教程使用说明(三)
这篇tkinter控件使用教程是最后一 一、TreeView 属性/事件描述代码实例columns列名,用于设置树形视图的列tree["columns"] ("姓名", "年龄", "性别")column列的属性,包括列名、宽度等tree.column("姓名…...

51c自动驾驶~合集58
我自己的原文哦~ https://blog.51cto.com/whaosoft/13967107 #CCA-Attention 全局池化局部保留,CCA-Attention为LLM长文本建模带来突破性进展 琶洲实验室、华南理工大学联合推出关键上下文感知注意力机制(CCA-Attention),…...
AtCoder 第409场初级竞赛 A~E题解
A Conflict 【题目链接】 原题链接:A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串,只有在同时为 o 时输出 Yes 并结束程序,否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

【机器视觉】单目测距——运动结构恢复
ps:图是随便找的,为了凑个封面 前言 在前面对光流法进行进一步改进,希望将2D光流推广至3D场景流时,发现2D转3D过程中存在尺度歧义问题,需要补全摄像头拍摄图像中缺失的深度信息,否则解空间不收敛…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

让回归模型不再被异常值“带跑偏“,MSE和Cauchy损失函数在噪声数据环境下的实战对比
在机器学习的回归分析中,损失函数的选择对模型性能具有决定性影响。均方误差(MSE)作为经典的损失函数,在处理干净数据时表现优异,但在面对包含异常值的噪声数据时,其对大误差的二次惩罚机制往往导致模型参数…...
return this;返回的是谁
一个审批系统的示例来演示责任链模式的实现。假设公司需要处理不同金额的采购申请,不同级别的经理有不同的审批权限: // 抽象处理者:审批者 abstract class Approver {protected Approver successor; // 下一个处理者// 设置下一个处理者pub…...

springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...

C++_哈希表
本篇文章是对C学习的哈希表部分的学习分享 相信一定会对你有所帮助~ 那咱们废话不多说,直接开始吧! 一、基础概念 1. 哈希核心思想: 哈希函数的作用:通过此函数建立一个Key与存储位置之间的映射关系。理想目标:实现…...

【版本控制】GitHub Desktop 入门教程与开源协作全流程解析
目录 0 引言1 GitHub Desktop 入门教程1.1 安装与基础配置1.2 核心功能使用指南仓库管理日常开发流程分支管理 2 GitHub 开源协作流程详解2.1 Fork & Pull Request 模型2.2 完整协作流程步骤步骤 1: Fork(创建个人副本)步骤 2: Clone(克隆…...
基于 HTTP 的单向流式通信协议SSE详解
SSE(Server-Sent Events)详解 🧠 什么是 SSE? SSE(Server-Sent Events) 是 HTML5 标准中定义的一种通信机制,它允许服务器主动将事件推送给客户端(浏览器)。与传统的 H…...