【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("姓名…...
Electron 打包自定义NSIS脚本为安装向导增加自定义页面增加输入框
Electron 打包工具有很多,如Electron-build、 Electron Forge 等,这里使用Electron-build,而Electron-build使用了nsis组件来创建安装向导,默认情况nsis安装向导不能自定义安装向导界面,但是nsis提供了nsis脚本可以扩展…...
Idea2023创建Servlet项目
① Java EE 只是一个抽象的规范,具体实现称为应用服务器。 ② Java EE 只需要两个包 jsp-api.jar 和 servlet-api.jar,而这两个包是没有官方版本的。也就是说,Java 没有提供这两个包,只提供了一个规范。那么这两个包是谁提供的…...
Day57:WEB攻防-SSRF服务端请求Gopher伪协议无回显利用黑白盒挖掘业务功能点
目录 SSRF-原理&挖掘&利用&修复 SSRF无回显解决办法 SSRF漏洞挖掘 SSRF协议利用 http:// (常用) file:/// (常用) dict:// (常用) sftp:// ldap:// tftp:// gopher:// (…...
【Qt】使用Qt实现Web服务器(十):前端基础
1、简述 本人对HTML元素不熟悉,利用QtWebApp加载静态页面来熟悉下HTML元素。 2、测试代码 # a)main中创建 HttpListener new HttpListener(listenerSettings,new RequestMapper(&app),&app);#...
使用vuepress搭建个人的博客(一):基础构建
前言 vuepress是一个构建静态资源网站的库 地址:VuePress 一般来说,这个框架非常适合构建个人技术博客,你只需要把自己写好的markdown文档准备好,完成对应的配置就可以了 搭建 初始化和引入 创建文件夹press-blog npm初始化 npm init 引入包 npm install -D vuepress…...
ArcGIS Pro导出布局时去除在线地图水印
目录 一、背景 二、解决方法 一、背景 在ArcGIS Pro中经常会用到软件自带的在线地图,但是在导出布局时,图片右下方会自带地图的水印 二、解决方法 解决方法:添加动态文本--服务图层制作者名单,然后在布局中选定位置添加 在状…...
启动mysql
删除C:\Program Files (x86)\MySQL\MySQL Server 5.7这个路径下的data文件夹,这个很难删除,因为一开机,mysql的某些服务就启动了,每次重新启动mysql之前,都要删除这个文件夹 因为这个文件夹在后端执行一些我们看不到的…...
C++实现二叉搜索树的增删查改(非递归玩法)
文章目录 一、二叉搜索树的概念结构和时间复杂度二、二叉搜索树的插入三、二叉搜索树的查找四、二叉搜索树的删除(最麻烦,情况最多,一一分析)3.1首先我们按照一般情况下写,不考虑特殊情况下4.1.1左为空的情况ÿ…...
软件架构复用
1.软件架构复用的定义及分类 软件产品线是指一组软件密集型系统,它们共享一个公共的、可管理的特性集,满足某个特定市场或任务的具体需要,是以规定的方式用公共的核心资产集成开发出来的。即围绕核心资产库进行管理、复用、集成新的系统。核心…...
【初阶数据结构】——leetcode:160. 相交链表
文章目录 1. 题目介绍2. 思路1:暴力求解算法思想代码实现 3. 思路2:快慢指针算法思想代码实现 1. 题目介绍 链接: link 给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&…...
惠州网站建设如何/百度官方首页
一,IOC容器初始化入口 Spring的容器的启动方式有很多种,上篇博文中有做过分析,Spring容器的初始化根本还是ApplicationContext的实例化过程,表现形式的多样化决定了容器不同的初始化方式,但是方式差异性主要还是体现在…...
如何判断网站做的关键词/宁德市区哪里好玩
此功能是通过shell32.dll中一个索引号为60的API函数调用,显示"关闭Windows"对话窗口实现的。具体方法为IDC_SHUTDOWNCOMPUTER按钮添加BN_CLICKED消息处理函数:void CControlDlg::OnShutdowncomputer() { HINSTANCE hInstLoadLibrary("…...
wordpress放在其他端口/网络推广方法怎么做
第一步:OTA升级原理解释 TI官方WIKI详细介绍 http://processors.wiki.ti.com/index.php/OAD 1 解释:2 第一步:红色方框 1 Boot就像PC的BIOS,负责选择要运行的Image,是Image-A,还是Image-B.就像…...
wordpress实现伪静态/上海短视频推广
前几天有网友问在输入坐标或长度的时候是否能输入公式,比如20/3或7*8这样简单的算式。cad虽然在定位点或长度时不能直接输入算式,但利用计算器功能不仅可以输入数字的算式,还可以输入点之前的算式,点可以是直接拾取的点࿰…...
永州建设网站/百度电话怎么转人工
1.本章学习总结 1.1 思维导图 1.2本章学习体会,代码量学习体会 1.2.1学习体会 初步了解什么是C语言,明白了这门语言的基本运行功能。了解了关于c语言结构上,语法上的基本知识。下一步要进一步深入挖掘这门语言的深度。编程是细致活࿰…...
wordpress 最大上传文件大小 8m/网络推广项目计划书
使用 Go 中的 SQL database 是容易的,只需下列这三步:// 步骤 1:导入主要的 SQL 包 import "database/sql"// 步骤 2:导入一个驱动包来明确要使用的 SQL 数据库 import _ "github.com/mattn/go-sqlite3"// 步…...