【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("姓名…...
iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...
【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
Cinnamon修改面板小工具图标
Cinnamon开始菜单-CSDN博客 设置模块都是做好的,比GNOME简单得多! 在 applet.js 里增加 const Settings imports.ui.settings;this.settings new Settings.AppletSettings(this, HTYMenusonichy, instance_id); this.settings.bind(menu-icon, menu…...
Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...
【开发技术】.Net使用FFmpeg视频特定帧上绘制内容
目录 一、目的 二、解决方案 2.1 什么是FFmpeg 2.2 FFmpeg主要功能 2.3 使用Xabe.FFmpeg调用FFmpeg功能 2.4 使用 FFmpeg 的 drawbox 滤镜来绘制 ROI 三、总结 一、目的 当前市场上有很多目标检测智能识别的相关算法,当前调用一个医疗行业的AI识别算法后返回…...
AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
Java多线程实现之Runnable接口深度解析
Java多线程实现之Runnable接口深度解析 一、Runnable接口概述1.1 接口定义1.2 与Thread类的关系1.3 使用Runnable接口的优势 二、Runnable接口的基本实现方式2.1 传统方式实现Runnable接口2.2 使用匿名内部类实现Runnable接口2.3 使用Lambda表达式实现Runnable接口 三、Runnabl…...
虚幻基础:角色旋转
能帮到你的话,就给个赞吧 😘 文章目录 移动组件使用控制器所需旋转:组件 使用 控制器旋转将旋转朝向运动:组件 使用 移动方向旋转 控制器旋转和移动旋转 缺点移动旋转:必须移动才能旋转,不移动不旋转控制器…...
精益数据分析(98/126):电商转化率优化与网站性能的底层逻辑
精益数据分析(98/126):电商转化率优化与网站性能的底层逻辑 在电子商务领域,转化率与网站性能是决定商业成败的核心指标。今天,我们将深入解析不同类型电商平台的转化率基准,探讨页面加载速度对用户行为的…...
