哈希冲突

简介: 哈希冲突可通过优化哈希函数或采用冲突解决策略应对。开放寻址法通过线性、二次探查或双散列寻找空位,但易导致聚集,影响效率;链表法则在冲突位置构建链表,避免抢占,更适应动态数据,是常用方案之一。

对于哈希冲突这个问题,我们有两类解决方案:

● 一类是构造尽可能理想的 Hash 函数,使得 Hash 以后得到的数值尽可能平均分布,从而减少冲突发生的概率;
● 另一类是在冲突发生以后,通过「提供冲突解决方案」来完成存储和查找。最常用的两种冲突解决方案是「开放寻址法」和「链表法」。

下面,我就来介绍一下这两种方法,并且重点看看它们对检索效率的影响。

如何利用开放寻址法解决 Hash 冲突?

所谓「开放寻址法」,就是在冲突发生以后,最新的元素需要寻找新空闲的数组位置完成插入。那我们该如何寻找新空闲的位置呢?我们可以使用一种叫作 线性探查(Linear Probing)的方案来进行查找。

线性探查 的插入逻辑很简单:在当前位置发现有冲突以后,就顺序去查看数组的下一个位置,看看是否空闲。如果有空闲,就插入;如果不是空闲,再顺序去看下一个位置,直到找到空闲位置插入为止。

查询逻辑也和插入逻辑相似。我们先根据 Hash 值去查找相应数组下标的元素,如果该位置不为空,但是存储元素的 Key 和查询的 Key 不一致,那就顺序到数组下一个位置去检索,就这样依次比较 Key。如果访问元素的 Key 和查询 Key 相等,我们就在哈希表中找到了对应元素;如果遍历到空闲处,依然没有元素的 Key 和查询 Key 相等,则说明哈希表中不存在该元素。

为了帮助你更好地理解,我们来看一个例子。

假设一个哈希表中已经插入了两个 Key,key1 和 key2。其中 Hash(key1) = 1, Hash(key2) = 2。这时,如果我们要插入一个 Hash 值为 1 的 key3。根据线性探查的插入逻辑,通过 3 步,我们就可以将 key3 插入到哈希表下标为 3 的位置中。插入的过程如下:

在查找 key3 的时候,因为 Hash(key3)= 1,我们会从哈希表下标为 1 的位置开始顺序查找,经过 3 步找到 key3,查询结束。

讲到这里,你可能已经发现了一个问题:当我们插入一个 Key 时,如果哈希表已经比较满了,这个 Key 就会沿着数组一直顺序遍历,直到遇到空位置才会成功插入。查询的时候也一样。但是,顺序遍历的代价是 O(n),这样的检索性能很差。

更糟糕的是,如果我们在插入 key1 后,先插入 key3 再插入 key2,那 key3 就会抢占 key2 的位置,影响 key2 的插入和查询效率。因此,线性探查会影响哈希表的整体性能,而不只是 Hash 值冲突的 Key。

为了解决这个问题,我们可以使用 二次探查(Quadratic Probing)和 双散列(Double Hash)这两个方法进行优化。下面,我来分别解释一下这两个方法的优化原理。

● 二次探查就是将线性探查的步长从 i 改为 i^2:第一次探查,位置为 Hash(key) + 1^2;第二次探查,位置为 Hash(key) +2^2;第三次探查,位置为 Hash(key) + 3^2,依此类推。
● 双散列就是使用多个 Hash 函数来求下标位置,当第一个 Hash 函数求出来的位置冲突时,启用第二个 Hash 函数,算出第二次探查的位置;如果还冲突,则启用第三个 Hash 函数,算出第三次探查的位置,依此类推。

无论是二次探查还是双散列,核心思路其实都是在 发生冲突的情况下,将下个位置尽可能地岔开,让数据尽可能地 随机分散存储,来降低对不相干 Key 的干扰,从而提高整体的检索效率。

但是,对于开放寻址法来说,无论使用什么优化方案,随着插入的元素越多、哈希表越满,插入和检索的性能也就下降得越厉害。在极端情况下,当哈希表被写满的时候,为了保证能插入新元素,我们只能重新生成一个更大的哈希表,将旧哈希表中的所有数据重新 Hash 一次写入新的哈希表,也就是 Re-Hash,这样会造成非常大的额外开销。因此,在数据动态变化的场景下,使用开放寻址法并不是最合适的方案。

如何利用链表法解决 Hash 冲突?

相比开放寻址法,还有一种更常见的冲突解决方案,链表法。所谓 链表法,就是在数组中不存储一个具体元素,而是存储一个链表头。如果一个 Key 经过 Hash 函数计算,得到了对应的数组下标,那么我们就将它加入该位置所存的链表的尾部。

这样做的好处是,如果 key3 和 key1 发生了冲突,那么 key3 会通过链表的方式链接在 key1 的后面,而不是去占据 key2 的位置。这样当 key2 插入时,就不会有冲突了。最终效果如下图。

相关文章
|
存储 Java Serverless
一篇博客让你认识哈希冲突和解决方法
一篇博客让你认识哈希冲突和解决方法
|
存储 Java
【JAVA】处理哈希冲突的常见方法
【JAVA】处理哈希冲突的常见方法
|
9月前
|
运维 搜索推荐 安全
企业建站常用的建站系统选项指南
《2026中小企业数字化转型指南》显示,超30%中小微企业已建官网,官网成品牌宣传、转型升级与出海必备。建站不难,关键选对工具:SAAS平台(如阿里云)省心易用;CMS系统更灵活——PageAdmin强于站群与业务系统,ECShop专注电商,WordPress生态丰富但需防卡顿与安全风险,齐博稳重适用政务,织梦则存版权与安全隐患。按需选择,事半功倍!(239字)
590 7
|
10月前
|
存储 NoSQL Linux
Redis集群部署指南
本教程基于CentOS7详解Redis集群部署,涵盖单机安装、主从复制、哨兵高可用及分片集群搭建。通过多实例模拟真实环境,深入讲解配置、启动、主从切换与数据读写测试,助你掌握Redis分布式架构核心技能。
1672 0
|
传感器 机器学习/深度学习 人工智能
从仿真到现实:数字孪生解锁具身AI全景应用
Embodied AI正在重塑智能机器人系统的格局,尤其通过为复杂且动态的环境中的行动执行提供许多现实可行的解决方案。然而,具身AI需要生成大量数据用于训练和评估,以确保其与物理环境交互的安全性。因此,有必要构建一个成本效益高的模拟环境,能够从物理特性、物体属性及交互中提供充足的训练和优化数据。Digital Twins是工业5.0中的关键议题,它通过镜像真实世界对应体的状态和行动,实现对物理过程的实时监控、模拟与优化。本综述探讨了将数字孪生与具身AI结合的方式,通过将虚拟环境转化为动态且数据丰富的平台,弥合仿真与现实之间的差距。
1622 7
|
安全 Linux Android开发
Cisco Secure Client 5.1.11.388 发布,新增 Linux ARM64 和 RHEL 10 支持
Cisco Secure Client 5.1.11.388 发布 - 思科远程访问客户端
1997 0
|
消息中间件 监控 NoSQL
Redis脑裂问题详解及解决方案
Redis脑裂问题是分布式系统中常见的复杂问题,合理配置Redis Sentinel、使用保护模式、采用分布式锁机制以及优化网络和客户端连接策略等措施,可以有效预防和解决脑裂问题。通过深入理解Redis脑裂问题的成因和影响,采取相应的解决方案,能够提高系统的可用性和数据一致性,保障Redis集群的稳定运行。希望本文能帮助你更好地理解和应对Redis脑裂问题。
1785 2
|
传感器 算法 机器人
基于 IMU 的位姿解算
解算 IMU 采样数据的过程与惯导解算技术原理有关,而提高定位精度的方法主要依赖于IMU自身精度的提高和算法改进。
3233 0
|
JavaScript 算法 前端开发
nodejs18版本 npm run dev失败
在使用若依框架运行 `npm run dev` 时,若卡在 95% 并报错,通常是 Node.js 17+ 与 Webpack 的兼容性问题。原因是 OpenSSL 3 的加密算法变化导致依赖冲突。解决方法:Windows 下运行 `set NODE_OPTIONS=--openssl-legacy-provider`,macOS/Linux 使用 `export NODE_OPTIONS=--openssl-legacy-provider`,然后重新启动开发服务即可。此设置让 Node.js 启用旧版加密支持,恢复正常构建流程。
1686 0
|
存储 索引
一文理解哈希冲突四种解决方法
一文理解哈希冲突四种解决方法
3846 1
一文理解哈希冲突四种解决方法

热门文章

最新文章