【C++练级之路】【Lv.18】哈希表(哈希映射,光速查找的魔法)



快乐的流畅:个人主页


个人专栏:《算法神殿》《数据结构世界》《进击的C++》

远方有一堆篝火,在为久候之人燃烧!

文章目录

  • 引言
  • 一、哈希
    • 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之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

那么,下面介绍两种常见的哈希函数:

  1. 直接定址法
    • Hash(key) = A*key + B

优点:简单、均匀
缺点:需要事先知道key的分布情况

  1. 除留余数法
    • 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;
};

细节:

  1. 每个哈希数据,都要设置状态变量,以便区分
  2. 状态分为空,存在和删除,数据状态初始化为空

2.2 成员变量

template<class K, class V>
class HashTable
{
public:
protected:vector<HashData<K, V>> _tables;size_t _n = 0;//有效数据个数
};

细节:

  1. 哈希表底层一般使用数组(vector)
  2. 哈希表的有效数据个数_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;
}

细节:

  1. 先用key取模数组size,得到哈希地址hashi
  2. 然后沿当前位置向后找,直到该位置状态为空超出数组边界,才算找不到
  3. 如果该位置状态为存在且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;
}

细节:

  1. 先查找当前是否存在该值,如果存在,则不插入
  2. 用key取模数组size,得到哈希地址hashi
  3. 然后沿当前位置向后找,直到状态为空或删除,才插入

但是,上述情况是哈希表未满时,如果满了如何扩容?还有,一定要满了才扩容吗?

这里,我们引入负载因子的概念:α = 有效数据个数 / 哈希表长度

当负载因子越大,哈希冲突的概率就越大,同时发生哈希踩踏的概率也越大,对于开放定址法,应该控制负载因子小于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);
}

细节:

  1. 判断时左右同乘以10,避免比较浮点数而带来误差
  2. newsize为原本的2倍(本来应该是接近2倍的素数,这里简单起见没实现)
  3. 将原哈希表中的元素一一映射到新表中
  4. 最后交换旧表和新表(类似于拷贝构造的现代写法)

2.6 删除

bool Erase(const K& key)
{HashData<K, V>* ret = Find(key);if (ret){ret._state = DELETE;--_n;return true;}return false;
}

细节:

  1. 先查找当前是否存在该值,如果存在,则删除
  2. 这里的删除,只用将状态变量改为删除即可

以上讲解的查找和插入,它们所用的探测方法是线性探测(一个一个往后找),这种探测方法可能会造成大量的哈希冲突。

那么,有没有什么探测方法能缓解哈希冲突呢?有,那就是二次探测!

改法也很简单,以一小段代码举例:

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;//有效数据个数
};

细节:

  1. 数组(vector)中存储单链表的头结点指针
  2. 模板参数的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;
}

细节:

  1. 先取模计算出哈希地址
  2. 再沿当前单链表向下查找

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. 先查找当前是否存在该值,如果存在,则不插入
  2. 取模计算出哈希地址,再头插新节点

运用开链法后,虽然没有哈希冲突了,但是链表长度过长也会影响效率。所以,哈希表也需要通过扩容来使链表长度变短,理想的状态是负载因子为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);}

细节:

  1. 二倍扩容(本来应该是接近2倍的素数,这里简单起见没实现)
  2. 遍历旧表,将旧表结点重新映射到新表上(这里直接链接,而不是创建新节点)
  3. 最后交换旧表和新表

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;
}

细节:

  1. 单链表删除,设置prev前置指针
  2. 注意头删的情况,分类处理

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;}
};

细节:

  1. 第一个哈希化函数,针对的是内置类型(整型或浮点型等),返回值设置为size_t,相近类型会进行隐式类型转换
  2. 第二个哈希化函数,针对的是字符串,运用了模板的特化。同时,为了防止字符串的异位串(对应字符数相同,而位置不同),并不是直接相加,而是每次相加后乘以31,保证肯定不重复。
  3. 同时,如果针对特殊的类,用户可以手写一个特定的哈希化函数进行模板传参

总结

相比闭散列,开散列看似增加了存储指针的空间开销,实际上闭散列要保证大量的空闲单元以降低哈希冲突,所以开散列反而更加节省空间,其空间利用率更高


哈希表与红黑树的对比:

  • 哈希表平均查找可达O(1),但最坏降到O(N)(哈希冲突)
  • 红黑树最坏查找也可保持O(logN),比较稳定

数据有序性:哈希表无序,而红黑树有序

适用场景:哈希表适合单点查找,红黑树适合范围查找


真诚点赞,手有余香

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/791543.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2013年认证杯SPSSPRO杯数学建模A题(第二阶段)护岸框架全过程文档及程序

2013年认证杯SPSSPRO杯数学建模 A题 护岸框架 原题再现&#xff1a; 在江河中&#xff0c;堤岸、江心洲的迎水区域被水流长期冲刷侵蚀。在河道整治工程中&#xff0c;需要在受侵蚀严重的部位设置一些人工设施&#xff0c;以减弱水流的冲刷&#xff0c;促进该处泥沙的淤积&…

余集和拉格朗日定理

L&#xff1a;一个群的例子&#xff08;在下面的文章中进一步详细介绍&#xff09;;R&#xff1a;约瑟夫路易拉格朗日&#xff08;1736-1813&#xff09;&#xff0c; 一、说明 数学家总是痴迷于根据乍一看似乎完全无关的事实/观察来形成概括。为什么&#xff1f;原因很简单&am…

基于 Quartz.NET 可视化任务调度平台 QuartzUI

一、简介 QuartzUI 是基于 Quartz.NET3.0 的定时任务 Web 可视化管理&#xff0c;Docker 打包开箱即用、内置 SQLite 持久化、语言无关、业务代码零污染、支持 RESTful 风格接口、傻瓜式配置、异常请求邮件通知等。 二、部署 QuartzUI 从 2022 年到现在没有提交记录&#xf…

第十二篇【传奇开心果系列】Python自动化办公库技术点案例示例:深度解读Python自动化操作Word

传奇开心果系列博文 系列博文目录Python自动化办公库技术点案例示例系列 博文目录前言一、Python自动化操作Word介绍二、使用python-docx示例代码二、**使用win32com示例代码**三、使用comtypes示例代码四、使用docx-mailmerge示例代码五、基本操作示例代码六、高级操作示例代码…

计算机网络——32差错检测和纠正

差错检测和纠正 错误检测 EDC 差错检测和纠错位&#xff08;冗余位&#xff09; D 数据由差错检测保护&#xff0c;可以包含头部字段 错误检测不是100%可靠的 协议会泄露一些错误&#xff0c;但是很少更长的EDC字段可以得到更好的检测和纠正效果 奇偶校验 单bit奇偶校验 …

精品PPT-2023年无人驾驶汽车车联网网络安全方案

以下是部分PPT内容&#xff0c;请您参阅。如需下载完整PPTX文件&#xff0c;请前往星球获取&#xff1a; 无人驾驶安全架构是一个复杂的系统&#xff0c;它涉及到多个关键组件和层次&#xff0c;以确保无人驾驶车辆在各种情况下都能安全、可靠地运行。以下是一些主要的无人驾驶…

基于springboot+vue的高效学生实习管理系统【管理员、学院、教师、企业单位、学生】

【管理员】 【院系负责人】 【教师】 【企业单位】 【学生】

Android Glide配置AppGlideModule定制化线程池,Kotlin(1)

Android Glide配置AppGlideModule定制化线程池&#xff0c;Kotlin&#xff08;1&#xff09; plugins {id org.jetbrains.kotlin.kapt }implementation com.github.bumptech.glide:glide:4.16.0kapt com.github.bumptech.glide:compiler:4.16.0 import android.content.Context…

【爬虫开发】爬虫从0到1全知识md笔记第3篇:数据提取概要,知识点【附代码文档】

爬虫开发从0到1全知识教程完整教程&#xff08;附代码资料&#xff09;主要内容讲述&#xff1a;爬虫课程概要&#xff0c;爬虫基础爬虫概述,,http协议复习。requests模块&#xff0c;requests模块1. requests模块介绍,2. response响应对象,3. requests模块发送请求,4. request…

YOLO电动车检测识别数据集:12617张图像,yolo标注完整

YOLO电动车检测识别数据集&#xff1a;12617张图像&#xff0c;电动车一类&#xff0c;yolo标注完整&#xff0c;部分图像应用增强。 适用于CV项目&#xff0c;毕设&#xff0c;科研&#xff0c;实验等 需要此数据集或其他任何数据集请私信

即刻体验 | 使用 Flutter 3.19 更高效地开发

我们已隆重推出全新的 Flutter 版本——Flutter 3.19。此版本引入了专为 Gemini 设计的新 Dart SDK、一个能让开发者对 Widget 动画实现精细化控制的全新 Widget&#xff0c;Impeller 更新带来的渲染性能提升、有助于实现深层链接的工具和对 Windows Arm64 的支持&#xff0c;以…

全局统一返数据类型封装记录

全局统一返回值封装 ​ 在Spring Boot中&#xff0c;实现全局统一返回值封装是一种常见的做法&#xff0c;它有助于保持API的一致性&#xff0c;并简化前端对响应数据的处理。创建一个响应体类&#xff0c;包含状态码、消息、数据等字段。这个类可以作为所有控制器返回值的通用…

达梦配置ODBC连接

达梦配置ODBC连接 基础环境 操作系统&#xff1a;Red Hat Enterprise Linux Server release 7.9 (Maipo) 数据库版本&#xff1a;DM Database Server 64 V8 架构&#xff1a;单实例1 下载ODBC包 下载网址&#xff1a;https://www.unixodbc.org/ unixODBC-2.3.0.tar.gz2 编译并…

python-django物流仓储进销存配送管理系统flask_1ea2k

实现了一个完整的物流管理系统&#xff0c;其中主要有站点信息模块、物流进度模块、用户表模块、司机模块、入库信息模块、签收信息模块、类型模块、快递信息模块、客户模块、客服模块、公告信息模块、服务类型模块、配置文件模块、出库信息模块、车辆信息模块、仓管模块、账户…

生成式AI的情感实验——AI能否产生思想和情感?

机器人能感受到爱吗&#xff1f;这是一个很好的问题&#xff0c;也是困扰了科学家们很多年的科学未解之谜。虽然我们尚未准备好向智能机器赋予情感&#xff0c;但智能机器却已经可以借助生成式人工智能&#xff08;AI&#xff09;来帮助我们表达自己的情感。 自然情感表达 AI正…

risc-v向量扩展strlen方法学习

riscv向量文档中给出了strlen的实现&#xff0c; 大概是这么一个思路&#xff0c; 加载向量: 使用向量加载指令&#xff08;如 vload&#xff09;从内存中加载一个向量长度的字符。比较向量与零: 使用向量比较指令&#xff08;如 vmask 或 vcmpeq&#xff09;来检查向量中的每…

Linux网络协议栈从应用层到内核层④

文章目录 1、网卡接受数据2、网络设备层接收数据3、ip层接受数据4、tcp层接受数据5、上层应用读取数据6、数据从网卡到应用层的整体流程 1、网卡接受数据 当网卡收到数据时&#xff0c;会触发一个中断&#xff0c;然后就会调用对应的中断处理函数&#xff0c;再做进一步处理。…

pycharm

✅作者简介&#xff1a;CSDN内容合伙人、阿里云专家博主、51CTO专家博主、新星计划第三季python赛道Top1&#x1f3c6; &#x1f4c3;个人主页&#xff1a;hacker707的csdn博客 &#x1f525;系列专栏&#xff1a;零基础学Python &#x1f4ac;个人格言&#xff1a;不断的翻越一…

企业计算机服务器中了mallox勒索病毒怎么办?Mallox勒索病毒解密流程步骤

网络技术的不断应用与发展&#xff0c;为企业的生产运营带来了极大便利&#xff0c;企业通过网络办公开展各项工作业务已经成为常态&#xff0c;网络为企业的发展带来了更多机遇&#xff0c;但随之而来的网络数据安全威胁也在不断增加。近期&#xff0c;云天数据恢复中心接到山…

19k star, 导出微信聊天记录,生成 HTML、Word、CSV等格式,并分析聊天数据,做成可视化年报

19k star, 导出微信聊天记录&#xff0c;生成 HTML、Word、CSV等格式&#xff0c;并分析聊天数据&#xff0c;做成可视化年报 分类 开源分享 项目名: 留痕 -- 一款强大的微信聊天管理工具 Github 开源地址&#xff1a;https://github.com/LC044/WeChatMsg Gitee 开源地址&…