【C++】————哈希表
作者主页: 作者主页
本篇博客专栏:C++
创作时间 :2024年8月6日
前言:
在计算机科学的广袤世界中,数据结构犹如基石,支撑着各种高效算法的构建与运行。而哈希表(Hash Table),作为其中一颗璀璨的明珠,以其独特的魅力和卓越的性能,在众多数据存储和检索场景中大放异彩。
哈希表,这个看似神秘却又充满力量的概念,其实与我们的日常生活息息相关。想象一下,当您在搜索引擎中输入关键词,瞬间就能获取到海量相关信息;当您在电商平台上浏览商品,系统能够迅速为您推荐符合您喜好的物品。这背后,哈希表都在默默发挥着关键作用。
哈希表的神奇之处在于它能够在平均情况下以接近常数的时间复杂度完成数据的插入、查找和删除操作,这使得它在处理大规模数据时具有极高的效率。然而,哈希表并非完美无缺,其也面临着哈希冲突、负载因子调整等一系列挑战。
在接下来的博客中,我们将深入探索哈希表的内部原理,剖析其工作机制,探讨如何优化哈希函数以减少冲突,研究不同的冲突解决策略,以及了解哈希表在实际编程中的广泛应用。无论您是初涉编程领域的新手,还是经验丰富的开发者,相信通过对哈希表的深入了解,都将为您的编程技能增添新的利器,让您在解决实际问题时更加游刃有余。
让我们一同踏上这段充满挑战与惊喜的哈希表之旅,揭开其神秘的面纱,领略数据结构的无穷魅力!
一、unordered系列关联式容器
在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达log2N,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同
1.1unordered_map
unordered_map在线文档说明
- unordered_map是存储<key,value>键值对的关联式容器,它允许通过key快速访问对应的value值
- 在unordered_map中,键值通常用于唯一的标识元素,而映射值是一个对象,其内容与此键关联,键值和映射值的类型可以不同
- unordered_map容器通过key访问单个元素比map要快,但他通常在遍历元素的方面效率较低
- unordered_map容器实现了operator[],可以使用key直接访问对应的value
- 它的迭代器至少是前向迭代器
1.2unordered_set
unordered_set在线文档说明
- unordered_set中的每个元素都是唯一的,因为它不允许有重复的元素
- 元素的存储顺序是不确定的,它取决于当前的哈希值和容器当前的哈希表的状态
- 由于使用的哈希表,它提供了平均情况下常数时间复杂度的查找、插入和删除操作
这里介绍的两个unordered系列的关联式容器和map和set还是有点相似的,我们再来几道题目来熟练掌握它们的使用
重复n次的元素
两个数组的交集
二、底层结构
unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构
哈希概念:
顺序结构已经平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找的时间复杂度为O(N),平衡树中为树的高度,搜索的效率取决于搜索过程中元素的比较次数
可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素,这就是最理想的搜索方法
在该结构中插入,查找元素时:
- 插入元素: 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 搜索元素: 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
注意:哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快,但是有成千上万的数,总会有几个数,取余后相等,那我们该怎么存放值呢?
hash(5) = 5 % 10 = 5;
hash(55) = 55 % 10 = 5;
这时就要引入一个新的概念 -> 哈希冲突
哈希冲突的概念:
我们把把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”
注意:哈希函数的设计目标是尽量减少冲突,但完全避免冲突几乎是不可能的
哈希函数:
引起哈希冲突的一个原因可能是:哈希函数设计不够合理
哈希函数设计原则
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
哈希冲突解决
解决哈希冲突两种常见的方法是:闭散列和开散列
闭散列:
闭散列: 也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去
线性探测
如果和上面讲的一样,现在需要插入元素55,先通过哈希函数计算哈希地址,hashAddr为5,
因此55理论上应该插在该位置,但是该位置已经放了值为5的元素,即发生哈希冲突
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入:
- 通过哈希函数获取待插入元素在哈希表中的位置
- 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
删除:
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素5,如果直接删除掉,5查找起来可能会受影响。因此线性探测采用标记的
伪删除法
来删除一个元素// 哈希表每个空间三种状态 // EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除 enum State {EMPTY, EXIST, DELETE };
线性探测的实现:
template<class K, class V>
struct HashData
{pair<K, V> _kv;Status _s;
};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){size_t newSize = _tables.size() * 2;HashTable<K, V, Hash> newHT;newHT._tables.resize(newSize);// 遍历旧表for (size_t i = 0; i < _tables.size(); i++){if(_tables[i]._s == EXIST){// 复用InsertnewHT.Insert(_tables[i]._kv);}}// 交换两个表的数据_tables.swap(newHT._tables);}Hash hf;// 线性探测size_t hashi = hf(kv.first) % _tables.size();while (_tables[hashi]._s == EXIST){hashi++;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._s = EXIST;++_n;return true;}HashData<K, V>* Find(const K& key){Hash hf;size_t hashi = hf(key) % _tables.size();while (_tables[hashi]._s != EMPTY){if (_tables[hashi]._kv.first == key){return &_tables[hashi];}hashi++;hashi %= _tables.size();}return NULL;}// 伪删除法bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){ret->_s = DELETE;--_n;return true;}else{return false;}}
private:vector < HashData<K, V>> _tables;size_t _n = 0; // 储存的关键字数据的个数
};
关于哈希表的取余
当我们的key不是整形的时候(常见的是string
),我们该怎么计算它的hashi
? 这里又得依靠我们的仿函数HashFunc
,又因为我们string
也是很常见的,我们将模板特化一下
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 e : key){// 避免因为顺序不一样而产生一样的值 BKDR// 避免 abc,acb同值不同意e *= 31;hash += e;}return hash;}
};
- 线性探测优点:实现非常简单
- 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低
开散列
开散列: 又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
注意:开散列中每个桶中放的都是发生哈希冲突的元素
开散列实现
template<class K, class V>
struct HashNode
{HashNode* _next;pair<K, V> _kv;HashNode(const pair<K, V>& kv):_kv(kv),_next(nullptr){}
};template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{typedef HashNode<K, V> Node;public:HashTable(){_tables.resize(10);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* _next = cur->_next;delete cur;cur = _next;}_tables[i] = nullptr;}}bool Insert(const pair<K, V>& kv){Hash hf;if (Find(kv.first)){return false;}// 负载因子if (_n == _tables.size()){vector<Node*> newTables;newTables.resize(_tables.size() * 2);// 遍历旧表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 挪动到新表size_t hashi = hf(cur->_data) % newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);}size_t hashi = hf(kv.first) % _tables.size();Node* newnode = new Node(kv);// 头插newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}Node* Find(const K& key){Hash hf;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first) == key){return cur;}cur = cur->_next;}return nullptr;}bool Erase(const K& key){Hash hf;size_t hashi = hf(key) % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr; // 记录上一个节点while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}prev = cur;cur = cur->_next;}return false;}private:vector<Node*> _tables;size_t _n = 0;
};
开散列增容:
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可
能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希
表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,
再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可
以给哈希表增容
if (_n == _tables.size())
{vector<Node*> newTables;newTables.resize(_tables.size() * 2);// 遍历旧表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 挪动到新表size_t hashi = hf(cur->_data) % newTables.size();cur->_next = newTables[hashi];newTables[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTables);
}
开散列与闭散列比较:
- 应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间
最后:
十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:
1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。
2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。
3.成年人的世界,只筛选,不教育。
4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。
5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。
最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)
愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!
相关文章:

【C++】————哈希表
作者主页: 作者主页 本篇博客专栏:C 创作时间 :2024年8月6日 前言: 在计算机科学的广袤世界中,数据结构犹如基石,支撑着各种高效算法的构建与运行。而哈希表(Hash Table)&#…...
前端学习AI历程
AI基本概念tensorflow入门conda搭建环境,pycham使用训练自己的第一个模型AI目前前端方便入手的几个方向 素材图片库图像识别,在线学习低代码应用智能客服 获取数据集 roboflowkagglecocomakesense(用于打标) 认识yolo两个简单小应…...

常见中间件漏洞复现之【Tomcat】!
Tomcat介绍 tomcat是⼀个开源⽽且免费的jsp服务器,默认端⼝ : 8080,属于轻量级应⽤服务器。它可以实现 JavaWeb程序的装载,是配置JSP(Java Server Page)和JAVA系统必备的⼀款环境。 在历史上也披露出来了很多的漏洞 …...

C++并发编程(一):线程基础
简介 本文学习的是 b 站 up 恋恋风辰的并发编程教学视频做的一些笔记补充。 教程视频链接如下:线程基础:视频教程 文档链接如下:线程基础:笔记文档 理论上直接看 up 提供的笔记文档即可,我这里主要是记录一些我自己…...

enq: HW - contention事件来啦
业务系统反应数据库慢,根据时间查看awr报告。 先看一眼事件名称 HW enqueue 用于序列化超出段高水位线的空间分配。如果同时向对象添加大量数据,则多个进程可能同时尝试在高水位线上方分配空间,从而导致争用。 既然是控制资源并发的enq&…...

MyBatis补充
控制类和dao层接口以及mapper中的xml是怎样的关联的? 在Mybatis中,控制类和dao层接口是通过mapper的xml文件进行连接的。 控制类调用dao层接口中的方法,通过接口实现进行访问数据库操作。dao层接口定义数据库操作的方法,提供给控制…...
系统架构师(每日一练16)
每日一练 答案与解析 1.软件测试一般分为两个大类:动态测试和静态测试。前者通过运行程序发现错误,包括()等方法;后者采用人工和计算机辅助静态分析的手段对程序进行检测,包括()等方法。答案与解析 问题1 A.边界值分析、逻辑覆盖、基本路径 B.桌面检查、…...

实践致知第17享:电脑忽然黑屏的常见原因及处理方法
一、背景需求 小姑电话说:最近,电脑忽然就黑屏了(如下图所示),但是等待几十秒甚至一分钟,电脑就能自然恢复了,这种状况一天能出现三四次,怎么办? 二、分析诊断 电脑黑屏…...

微信小程序--实现地图定位---获取经纬度
(1) (2) (3) html: <view class"titleTwo" style"border: none;"><view class"fontSize30 invoiceTile">企业地址</view><view class"invoiceRight" bind:tap"tapChooseAddress" data-maptype"…...

【Python系列】使用 `isinstance()` 替代 `type()` 函数
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...

【多模态大模型】 BLIP-2 in ICML 2023
一、引言 论文: BLIP-2: Bootstrapping Language-Image Pre-training with Frozen Image Encoders and Large Language Models 作者: Salesforce Research 代码: BLIP-2 特点: 该方法分别使用冻结的图像编码器(ViT-L/…...
HPC高性能计算平台
随着技术的发展和数据量的爆炸性增长,企业面临的挑战日益复杂,对计算能力的需求也在不断增加。这些问题的解决超出了传统计算方法的能力范围,高性能计算(HPC)正是为解决这类问题而生。 高性能计算(HPC&…...

前端常用的几个工具网站
觉得不错的前端工具类网站 1、Grid布局生成 https://cssgrid-generator.netlify.app 2、拟物按钮样式生成 https://neumorphism.io 3、玻璃形态效果 在线制作CSS玻璃形态 4、一些Button、checkBox、switch、card的css样式 零代码 - 精美CSS样式库 5、CSS阴影生成 在线创建…...
支付功能之代收代付
有很多老板问小编:“这个分账功能好是好,也能搞定项目中的二清问题和税务纠纷,但还是太复杂了,每次要添加被分账对象都需要提交材料进行审核,太繁琐了,有没有更方便快捷的支付产品来解决资金问题࿱…...
QPixmap
pixel[ˈpɪksl]像素 QPixmap 是 Qt 框架中用于处理图像的一个类。它主要用于在屏幕上显示和处理图像,提供了许多实用的功能,如加载、保存、缩放、旋转、合并等。 绘制 从文件加载:从指定文件加载图像。 QPixmap pixmap(":/images/exam…...
Laravel门面之下:构建自定义门面应用的艺术
Laravel门面之下:构建自定义门面应用的艺术 在Laravel框架中,门面(Facade)提供了一种将类静态调用与面向对象代码解耦的优雅方式。门面是一个全局可访问的类,它为底层复杂的服务提供了一个简单的接口。然而࿰…...

智启万象 | 2024 Google 开发者大会直播攻略
8 月 7 日上午 9:30 2024 Google 开发者大会 主旨演讲直播将准时开启 想要在线上探索大会精彩内容? 快查收这份观看指南! 8 月 7 日上午 9:30 2024 Google 开发者大会开幕 锁定大会官网观看主旨演讲现场直播! 本次大会内容将同步于多个…...
技巧:print打印内容到控制台时信息显示不全
# 请求一个接口,res是响应内容,使用res.text打印的信息不全 #使用流式处理响应 #如果你需要流式处理大的响应,确保你在处理响应内容的同时不会提前结束流。resself.request_base(select_api,change_datachange_data)print("")# pri…...

3.表的操作
目录 创建表 创建表案例: 查看表结构 修改表 1.增加新列 2.修改列的属性 3.删除列 4.修改表名 5.修改列 删除表 创建表 语法: CREATE TABLE [IF NOT EXISTS] table_name(field1 datatype1 [COMMENT 注释信息],field2 datatype2 [COMMENT 注释…...

AI回答:C#项目编译后生成部分文件的主要职责
【引入】以ConsoleApp1为例,请问C#编译之后以下文件有啥用 1.bin\runtimes 文件夹存放什么,有什么用? bin\runtimes 文件夹存放了项目的运行时相关文件,这些文件包括了各种目标平台的运行时库。 2.bin\生成的exe文件可以在别的电脑…...

Python:操作 Excel 折叠
💖亲爱的技术爱好者们,热烈欢迎来到 Kant2048 的博客!我是 Thomas Kant,很开心能在CSDN上与你们相遇~💖 本博客的精华专栏: 【自动化测试】 【测试经验】 【人工智能】 【Python】 Python 操作 Excel 系列 读取单元格数据按行写入设置行高和列宽自动调整行高和列宽水平…...
【磁盘】每天掌握一个Linux命令 - iostat
目录 【磁盘】每天掌握一个Linux命令 - iostat工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景 注意事项 【磁盘】每天掌握一个Linux命令 - iostat 工具概述 iostat(I/O Statistics)是Linux系统下用于监视系统输入输出设备和CPU使…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...

如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...

高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...

Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
蓝桥杯 冶炼金属
原题目链接 🔧 冶炼金属转换率推测题解 📜 原题描述 小蓝有一个神奇的炉子用于将普通金属 O O O 冶炼成为一种特殊金属 X X X。这个炉子有一个属性叫转换率 V V V,是一个正整数,表示每 V V V 个普通金属 O O O 可以冶炼出 …...
08. C#入门系列【类的基本概念】:开启编程世界的奇妙冒险
C#入门系列【类的基本概念】:开启编程世界的奇妙冒险 嘿,各位编程小白探险家!欢迎来到 C# 的奇幻大陆!今天咱们要深入探索这片大陆上至关重要的 “建筑”—— 类!别害怕,跟着我,保准让你轻松搞…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill
视觉语言模型(Vision-Language Models, VLMs),为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展,机器人仍难以胜任复杂的长时程任务(如家具装配),主要受限于人…...

Linux部署私有文件管理系统MinIO
最近需要用到一个文件管理服务,但是又不想花钱,所以就想着自己搭建一个,刚好我们用的一个开源框架已经集成了MinIO,所以就选了这个 我这边对文件服务性能要求不是太高,单机版就可以 安装非常简单,几个命令就…...