Java 面试宝典:你知道多少种解决 hash 冲突的方法?

news2024/11/18 2:35:51

大家好,我是大明哥,一个专注「死磕 Java」系列创作的硬核程序员。
本文已收录到我的技术网站:https://www.skjava.com。有全网最优质的系列文章、Java 全栈技术文档以及大厂完整面经


回答

在使用 hash 表时, hash 冲突是一个非常常见的问题,该问题出现的主要原因是两个不同的输入值,通过 hash 函数计算得到了相同的 hash 值,尝试存储在 hash 表的同一个位置。解决 hash 冲突主要有如下几种方式:

  1. 链地址法:解决 hash 冲突最经典的方法。它是通过将具有相同 hash 值的所有元素存储在同一个索引位置的链表中来解决冲突的。
  2. 开放定址法:在 hash 表的数组本身中找到一个空闲的槽位来存储冲突的元素。它的核心思想是:当发生 hash 冲突时,按照某种探测技术来探测下一个空闲的槽位。
  3. 再 hash 法:依赖多个 hash 函数来寻找空闲槽位,其思想是:当第一个 hash 函数 h1(x) 导致冲突时,系统将尝试第二个 hash 函数 h2(x),如果仍然冲突,将继续尝试第三个 hash 函数 h3(x),依此类推,直到找到一个空闲槽位为止。

详解

链地址法

链地址法是解决 hash 冲突最经典方法,Java 中的 HashMap 使用的就是它。它是通过将具有相同 hash 值的所有元素存储在同一个索引位置的链表中来解决冲突。这种方法允许多个条目存在于 hash 表的同一个位置,从而避免了冲突直接导致的存储问题。其结构如下:

其工作原理如下:

  1. 初始化 hash 表:创建一个 hash 表(数组),每个索引位置初始为空,用于存放链表的头节点。
  2. 定义 hash函数:设计一个 hash 函数,该函数将存储的键转换为 hash 表的索引。注意, hash 函数的设计对于性能至关重要,好的 hash 函数能够均匀分布键,从而减少冲突。
  3. 插入操作
    1. 使用 hash 函数计算出键的 hash 值,确定在 hash 表中的索引位置;
    2. 如果该索引位置没有链表存在,则创建一个新的链表,并将键值对作为链表的第一个节点添加到此索引位置;
    3. 如果该索引位置已存在链表,则将新的键值对节点添加到链表的末尾或头部;

查询和删除操作和插入步骤一直,先计算 hash 值得到索引位置,然后根据链表来查询或删除。

优缺点

  • 优点
    • 容易实现:链地址法的数据结构和算法相对简单,易于实现和理解。
    • 处理冲突比较灵活:链地址法通过链表来管理同一 hash 值的多个元素,能够灵活应对冲突,理论上链表可以无限延长。
    • 动态扩展:由于使用链表来存储相同 hash 值的元素,链地址法允许 hash 表动态增长。
    • 删除和添加高效:删除和添加都是在链表中完成,性能较高。
  • 缺点
    • 增加了内存开销:每个元素都需要额外的存储空间来存储指向链表中下一个元素的指针,增加了内存开销。
    • 性能依赖于 hash 函数:链地址法的性能在很大程度上取决于 hash 函数的质量,如果 hash 函数质量不高,则会导致大量数据聚集在少数的几个位置,性能会显著下降。

性能优化

  1. 使用高质量的hash 函数。
  2. 取代链表:当链表中的元素超过某个阈值时,则将链表结构调整为红黑树,改善查询效率。

开放定址法

开放定址法与链地址法不同,它是在 hash 表的数组本身中找到一个空闲的槽位来存储冲突的元素。它的核心思想是:当发生 hash 冲突时,按照某种探测技术来探测下一个空闲的槽位。其工作原理如下:

  1. 初始插入:使用 hash 函数计算数据的 hash 值,得到其在 hash 表中的索引位置。
  2. 冲突检测:如果该位置已经被占用,表明发生了 hash 冲突。
  3. 解决冲突:通过某种探测技术在 hash 表中寻找另一个空闲槽位。
  4. 数据插入:将数据插入到找到的空闲槽位中。

开放定址法主要通过以下三种探测技术解决冲突。

一、线性探测

当发生冲突时,顺序探查下一个槽位,直到找到一个空槽位。这种方法简单,但可能导致"聚集"现象,即连续的槽位被占用,从而影响后续插入和查找操作的效率。

比如我们使用大小为 8 的 hash 表,一次添加 5、10、2、4、18,如下:

详细过程如下:

  1. 插入5h(5) = 5 % 8 = 5,位置 5 是空的,所以 5 插入位置 5。
  2. 插入10h(10) = 10 % 8 = 2,位置 2 是空的,所以 10 插入位置 2。
  3. 插入2h(2) = 2 % 8 = 2,但位置 2 已被 10 占用,因此我们线性探测下一个位置,即位置 3,位置 3 是空的,所以 2 插入位置 3。
  4. 插入4:h(4) = 4 % 8 = 4,位置 4 是空的,所以 4 插入位置 4。
  5. 插入18h(18) = 18 % 8 = 2,位置 2 已被 10 占用,位置 3 被 2 占用,继续线性探测到位置 4,但位置 4 被4 占用,再次线性探测到位置 5,位置 5 已被 5 占用。继续探测到位置 6,位置 6 是空的,所以 18 插入位置 6。

二、二次探测

在发生冲突时,不是简单地检查下一个槽位,而是使用二次函数来计算探测的间隔。在二次探测中,如果第一个计算得到的 hash 地址已经被占用,将会尝试一个二次方程式来计算下一个地址,直到找到空位置。

二次探测减少了线性探测的“聚集”现象,但可能仍存在二次聚集问题。

如果我们的 hash 函数是 h(key) = key % table_size,那么在遇到冲突时,二次探测的探测序列会是这样的:

  • 第一次探测:h(key) + 1^2 % table_size
  • 第二次探测:h(key) + 2^2 % table_size
  • 第三次探测:h(key) + 3^2 % table_size

所以,使用上面的例子,二次探测结果如下:

详细过程如下:

  1. 插入5h(5) = 5 % 8 = 5,位置 5 空,放入 5。
  2. 插入10h(10) = 10 % 8 = 2,位置 2 空,放入 10。
  3. 插入2h(2) = 2 % 8 = 2,位置 2 已占,进行二次探测:2 + 1^2 = 3,位置 3 空,放入 2。
  4. 插入4h(4) = 4 % 8 = 4,位置 4 空,放入 4。
  5. 插入18h(18) = 18 % 8 = 2,位置 2 已占,进行二次探测:2 + 1^2 = 3,位置 3 已占,2 + 2^2 = 6,位置 6 空,放入 18。

三、双重 hash

使用两个 hash 函数。当第一个 hash 函数导致冲突时,使用第二个 hash 函数计算探测步长。这种方法通常能够更均匀地分布 hash 冲突,减少“聚集”现象,提高 hash 表的整体性能。其原理如下:

双重 hash 使用两个 hash 函数,记为 h1(x)h2(x)。当在 hash 表中插入一个元素时,首先使用第一个 hash 函数 h1(x) 确定元素的初始位置。如果该位置没有被占用,则直接插入。如果该位置已被占用(发生了冲突),则使用第二个 hash 函数 h2(x) 来计算探测序列的步长,然后按此步长在 hash 表中进行探测,直到找到空槽或目标元素。

具体公式如下:

p i = ( h 1 ( x ) + i ⋅ h 2 ( x ) )   m o d   M p_{i}=\left(h_{1}(x)+i \cdot h_{2}(x)\right) \bmod M pi=(h1(x)+ih2(x))modM

M 表示 hash 表的大小,h1(x) 表示第一个 hash 函数,h2(x) 表示第二个 hash 函数,i 表示探测的次数。

我们继续上面的例子:

  1. h1(x) = x % 8
  2. h2(x) = 5 - (x % 5):一般第二个 hash 函数最好能确保它产生的步长是非零的,且与表的大小互质(假设表的大小为 8,那么步长应该是与 8 互质的数)。

详细过程如下:

  1. 插入5
    1. 第一次尝试: 使用 h1(5) = 5 % 8 = 5,位置 5 是空的,所以 5 被直接插入到位置 5。
  2. 插入10
    1. 第一次尝试: 使用 h1(10) = 10 % 8 = 2,位置 2 是空的,因此 10 被直接插入到位置 2。
  3. 插入2
    • 第一次尝试: 使用 h1(2) = 2 % 8 = 2,位置 2 已被 10 占用。
    • 双重 hash 探测: 使用 h2(2) = 5 - (2 % 5) = 3,因此新位置计算为 (2 + 1*3) % 8 = 5,位置 5 已被 5 占用。
    • 继续探测: 更新探测 (2 + 2*3) % 8 = 0,位置 0 是空的,2 被插入到位置 0。
  4. 插入4
    1. 第一次尝试: 使用 h1(4) = 4 % 8 = 4,位置 4 是空的,因此 4 被直接插入到位置 4。
  5. 插入18
    • 第一次尝试: 使用 h1(18) = 18 % 8 = 2,位置 2 已被 10 占用。
    • 双重 hash 探测: 使用 h2(18) = 5 - (18 % 5) = 2,因此新位置计算为 (2 + 1*2) % 8 = 4,位置 4 已被 4 占用。
    • 继续探测: 更新探测 (2 + 2*2) % 8 = 6,位置 6 是空的,18 被插入到位置 6。

优点

  • 不需要额外的数据结构来存储数据,相比链地址法,使用的内存空间更加少。
  • 由于数据存储在连续的内存空间,所以在寻址时可能有更好的缓存性能。

缺点

  • 当 hash 表较满时,开放定址法的性能会显著下降,原因是空闲槽位的查找会变得比较困难。
  • 有“聚集”现象。

再哈希法

再哈希法与开放定址法中的双重 hash 差不多。再哈希法依赖多个 hash 函数来寻找空闲槽位,其思想是:当第一个 hash 函数 h1(x) 导致冲突时,系统将尝试第二个 hash 函数 h2(x),如果仍然冲突,将继续尝试第三个 hash 函数 h3(x),依此类推,直到找到一个空闲槽位为止。

继续上面例子,比如我们有如下三个 hash 函数:

  • h1(x) = x % 8
  • h2(x) = 7 - (x % 7)
  • h3(x) = 5 - (x % 5)

  1. 插入5h1(5) = 5 % 8 = 5,位置 5 空,放入 5。
  2. 插入10h1(10) = 10 % 8 = 2,位置 2 空,放入 10。
  3. 插入2
    1. h1(2) = 2 % 8 = 2,位置 2 已占
    2. 进行二次 hash:h2(2) = 7 - (2 % 7) = 5 ,位置 5 已占
    3. 进行三次 hash:h3(2) = 5 - (2 % 5) = 3,位置 3 空,放入 2
  4. 插入4:h(4) = 4 % 8 = 4,位置 4 空,放入 4。
  5. 插入18
    1. h1(18) = 18 % 8 = 2,位置 2 已占
    2. 进行二次 hash:h2(18) = 7 - (18 % 7) = 4,位置4 已占
    3. 进行三次 hash:h3(18) = 5 - (18 % 5) = 2,位置 2 已占,直接选择其他 hash 算法,比如这里大明哥就直接穷举了,从 hash 表 0 位置开始,位置 0 空,放入 18。

优点

  • 能够减少“聚集”现象:与单一 hash 函数相比,再哈希法通过多个 hash 函数减少了聚集现象,提高了 hash 表的均匀性。

缺点

  • 性能开销:虽然再哈希法可以减少冲突,但每次冲突时尝试多个 hash 函数会增加计算开销。
  • 依赖 hash 函数:每个 hash 函数都需要精心设计,以确保它们之间的独立性和生成的索引位置的均匀性。否则会格外的增加计算开销。

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

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

相关文章

01-Three.js

引入three.js 1.script标签引入 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><title>Three.js中文网&#xff1a;http://www.webgl3d.cn/</title><!-- 引入three.js --><script src"…

恢复MySQL!是我的条件反射,PXB开源的力量...

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

【Linux】账号和权限管理

目录 一、用户账号与组账号 二、添加用户账号-useradd 三、修改用户账号的属性-usermod 四、更改用户命令-passwd 五、删除用户账号-userdel 六、添加组账号-groupadd 七、添加删除组成员-gpasswd 八、删除组账号-groupdel 九、查询账号信息-groups、id、finger、w、w…

Opentelemetry——Signals-Baggage

Baggage Contextual information that is passed between signals 信号之间传递的上下文信息 In OpenTelemetry, Baggage is contextual information that’s passed between spans. It’s a key-value store that resides alongside span context in a trace, making values…

开源博客项目Blog .NET Core源码学习(15:App.Hosting项目结构分析-3)

本文学习并分析App.Hosting项目中前台页面的关于本站页面和点点滴滴页面。 关于本站页面 关于本站页面相对而言布局简单&#xff0c;与后台控制器类的交互也不算复杂。整个页面主要使用了layui中的面包屑导航、选项卡、模版、流加载等样式或模块。   面包屑导航。使用layui…

RuntimeError: Error(s) in loading state_dict for ZoeDepth解决方案

本文收录于《AI绘画从入门到精通》专栏,订阅后可阅读专栏内所有文章,专栏总目录:点这里。 大家好,我是水滴~~ 本文主要介绍在 Stable Diffusion WebUI 中使用 ControlNet 的 depth_zoe 预处理器时,出现的 RuntimeError: Error(s) in loading state_dict for ZoeDepth 异常…

20240414,类的嵌套,分文件实现

笑死&#xff0c;和宝哥同时生病了 一&#xff0c;封装-案例 1.0 立方体类 #include<iostream>//分别用全局函数和成员函数判定立方体是否相等 using namespace std;class Cube { public:int m_area;int m_vol;int geth(){return m_h;}int getl() { return m_l; }int…

vue 上传csv文件

index---------主页面&#xff08;图1&#xff09; form-----------子页面&#xff08;图2&#xff09; index.vue /** 重点&#xff01;&#xff01;&#xff01;&#xff01; * 获取表单组件传递的信息&#xff0c;传给后端接口 * param {从form表单传递的数据} datas * Fi…

反射与动态代理

一、反射 什么是反射? 反射允许对成员变量&#xff0c;成员方法和构造方法的信息进行编程访问 1.获取class对象的三种方式 Class这个类里面的静态方法forName&#xff08;“全类名”&#xff09;&#xff08;最常用&#xff09; 通过class属性获取 通过对象获取字节码文件对…

day9 | 栈与队列 part-1 (Go) | 232 用栈实现队列、225 用队列实现栈

今日任务 栈与队列的理论基础 (介绍:代码随想录)232 用栈实现队列(题目: . - 力扣&#xff08;LeetCode&#xff09;)225 用队列实现栈 (题目: . - 力扣&#xff08;LeetCode&#xff09; ) 栈与队列的理论基础 栈 : 先进后出 队列: 后进先出 老师给的讲解:代码随想录 …

stm32f103---按键控制LED---代码学习

目录 一、总体代码 二、LED端口初始化分析 ​编辑 三、LED灭的控制 四、LED亮 五、按键初始化 ​ 六、按键控制LED的功能 一、总体代码 这里使用到了LED灯和按键&#xff0c;实现效果是当按键按下时灯的亮灭转化 #include "stm32f10x.h" #include "bsp_led…

Jupyter Notbook如何安装配置并结合内网穿透实现无公网IP远程连接使用

文章目录 推荐1.前言2.Jupyter Notebook的安装2.1 Jupyter Notebook下载安装2.2 Jupyter Notebook的配置2.3 Cpolar下载安装 3.Cpolar端口设置3.1 Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&am…

《手把手教你》系列基础篇(八十五)-java+ selenium自动化测试-框架设计基础-TestNG自定义日志-下篇(详解教程)

1.简介 TestNG为日志记录和报告提供的不同选项。现在&#xff0c;宏哥讲解分享如何开始使用它们。首先&#xff0c;我们将编写一个示例程序&#xff0c;在该程序中我们将使用 ITestListener方法进行日志记录。 2.TestNG自定义日志 2.1创建测试用例类 1.按照宏哥前边的方法&…

TensorFlow中LSTM神经网络详解

TensorFlow中LSTM神经网络详解 一、LSTM神经元1.1 神经网络引入1.2 RNN神经元结构1.3 LSTM神经元1.3.1 LSTM模型框架1.3.2 隐藏态1.3.3 遗忘门1.3.4 记忆门1.3.5 输出门 二、LSTM神经网络2.1 LSTM网络架构 时间序列预测分析可以实现对未来数据的预测分析&#xff0c;通过分析过…

安装mamba_ssm报错

最近想跑一下VM-UNet的代码&#xff0c;结果发现需要安装mamba_ssm&#xff0c;于是我直接pip install mamba_ssm,发现报错&#xff0c;错误提示说需要安装cuda11.6及以上的版本。然后我就默默地安装cuda11.6&#xff0c;后来我才发现不用安装cuda11.6也可以。 在vmunet的gitu…

【笔记】mysql版本6以上时区问题

前言 最近在项目中发现数据库某个表的createTime字段的时间比中国时间少了13个小时&#xff0c;只是在数据库中查看显示时间不对&#xff0c;但是在页面&#xff0c;又是正常显示中国时区的时间。 排查 项目中数据库的驱动使用的是8.0.19&#xff0c;驱动类使用的是com.mysq…

快速入门深度学习9.1(用时20min)——GRU

速通《动手学深度学习》9.1 写在最前面九、现代循环神经网络9.1 门控循环单元&#xff08;GRU&#xff09;9.1.1. 门控隐状态9.1.1.1. 重置门和更新门9.1.1.2. 候选隐状态9.1.1.3. 隐状态 9.1.3 API简洁实现小结 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 20…

数据结构学习之路--一网打尽链表的相关操作(附C源码)

嗨嗨大家~我们今天继顺序表内容来讲解链表。话不多说&#xff0c;让我们走进本期的学习吧&#xff01; 目录 一、线性表的链式存储 1 链式存储结构 2 链表的定义 3 链表的分类 二、链表的实现过程 1 链表的打印 2 结点的创建 3 链表的头插 4 链表的头删 5 链表的…

vue列表列表过滤

对已知的列表进行数据过滤(根据输入框里面的内容进行数据过滤) 编写案例 通过案例来演示说明 效果就是这样的 输入框是模糊查询 想要实现功能&#xff0c;其实就两大步&#xff0c;1获取输入框内容 2根据输入内容进行数据过滤 绑定收集数据 我们可以使用v-model去双向绑定 …

LazyVim开发vue2

neovim 0.5刚出来的时代&#xff0c;那时刚有lua插件我很狂热。每天沉迷于打造自己的IDE之中。写过一堆相关的博客&#xff0c;也录过一些视频教程。后来发现neovim的接口和插件更新的很快&#xff0c;导致配置文件要不定期的修改&#xff0c;才能保证新版本的插件的适配。我也…