针对于HashMap的(n-1) hash的研究和拓展思考
创始人
2024-05-29 16:33:43
0

本文概览:

简单, 直白的看一看为什么 HashMap 中存在 (n-1) & hash 以及 n & hash.
思考题: 在 HashMap 中 hash 和 HashCode 有什么关系, 他们是一个东西吗吗?
HashCode 的两次扰动

本文能解决你的哪些问题?

  • HashMap 中 hash 和 hashcode 的关系分析 (这里很多博主没有理清楚)
  • 为什么插入的时候使用 (n -1) & hash 但是扩容的时候, 将数据进行分散 rehash (), 为什么又要使用 n & hash
  • 你了解 rehash ()? 方法的目的吗, 真的是重新计算 hash 吗? 真的不是计算其他的值吗?
  • HashMap 中的 hash 冲突, 指的是 hash 值完全相同吗??? 亦或者是 hashCode 相同

先来看看思考题吧

HashCode 是什么?Hash 又是什么
>前提: 需要了解, 我们使用 hashmap, 如果要提高他的性能, 是不是想让元素尽量的存储在 hashMap 的数组的不同位置上, 这样才能最大利用 Hash Map 来高效存储数据

看看 HashMap# int hash (),

当我们传入一个 key 的时候, 需要调用这个方法来计算 hash 值

    static final int hash(Object key) {int h;// 冷知识, key如果==null,存放在表的第一个位置// 至于为什么需要 异或运算 ^,先继续看return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

可以看见, 这个 hash 值, 在 HashMap 是由 hashCode 计算得到的, 所以说 hash != hashCode, 这一点在后面对于理解来说至关重要.
那么 <hash值> 有什么作用呢?
– 计算数据映射到 Hash Map 数组的位置

来看看插入

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node[] tab; Node p; int n, i;if ((tab = table) == null || (n = tab.length) == 0)// 注意这里n是hashMap数组的长度n = (tab = resize()).length;// 你只需要看这里,插入的时候,使用(n-1) & hash得到一个iif ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);.... 下面的源码不展示了}

这个 (n - 1) & hash 得到的是啥? 得到的是元素映射到 hashMap 数组的索引

先思考为什么能映射到数组的索引上, n-1 & hash,
例如 n == 16, 此时 n -1 == 15, hash 为 32 位数字.
此时 (n-1) & hash,
1111
00000000 00000000 00000000 11111111 (共 32 位)
此时结果就是 <hash的后四位吧>, 如果长度扩容到 32 了呢, 32 - 1 == 31, 31 的二进制是五位 <结果就是比较hash的后五位吧>
思考思考, 长度为 16, 四位二进制最高多少, 是不是 15, 同理
也就是说, (n-1) & hash 能让结果落在数组长度范围内

再来帮你梳理一下, n 是哈希表长度, 为二的倍数, 此时 (n-1)一定就是全 1, 只不过长度不一样, 15 是四个一, 31 是五个 1.
只要 n 为二的倍数, 那么 n-1 就是全一, 只不过位数不一样.
结果就是, 我们得到的 hash 是决定数组映射到数组哪一个位置上面的关键, 而最重要的是 hashCode 的后 x 位数字, 数据的插入到哪比较这个

让 hashcoe 的后 x 位变得乱七八糟!!

我们的目的是啥, 想让插入数据平均分布对吧, 那么我们就需要让 hash 变得没有规律, 才会平均的放置到数组的不同位置上.
此时, 就有两个操作, 一个是
h = key.hashCode()) ^ (h >>> 16 ) // h 为 32 位,>>>是右移 16 位, 此时高 16位移动到低 16 位, 然后这个二进制又和 key. HashCode () 进行异或操作, 这里的操作是不是 很乱, 很乱就对了啊, 结果也会乱, 也很随机
然后就是 (n-1) & hash, 我们刚才说了啥, 是不是比较的就是 <hash后几位>, OK, 现在目的达到了, 结果变得很乱, 那么数据的分布也就变得分散了.

上面你已经知道了, 得到一个元素的索引, 其实看的就是他的 hash 的后 x 位,.
这里很重要, 很多博主都没有理清楚, 大家如果能看到这里, 其实也都懂数据结构中的哈希表是什么(这里指的不是 HashMap, 只是普通的哈希表数据结果), 我们所说的哈希冲突, 指的就是哈希冲突, 也就是哈希值相同, 导致两个元素需要放到一个链表上面.
<经过上面的分析,你觉得HashMap中的哈希碰撞,是hash相等吗??,哈希冲突本质上在hashMap中其实是 hash的后x位数字相同,>
举例: 比如说 n == 16, n - 1 == 15 对吧, 此时我给你两个不同的 hash 值, 你来计算看看结果是啥
Hash 1 : … 前面省略 24 位 00001111
Hash 2: … 前面省略 24 位 11111111
结果显而易见, 他们俩 hash 值相同, 但是 ( n-1) & hash 都相等啊, 也就是说两个不同的 hash 值的数据, 如果后 x 位 (这个 x 的长度为 n-1 的二进制长度)

你怎么还没讲到 n & hash 啊, 马上就讲了, 马上就讲了

不过啊, 还得了解一个事情, 就是扩容是如何扩容的?
首先我默认你知道 HashMap 每次扩容两倍? 然后 HashMap 扩容之后还会把一些数据映射到其他位置对吧, 也就是 rehash ()

    final Node[] resize() {... 前面的源码就不看了if (oldTab != null) {for (int j = 0; j < oldCap; ++j) {Node e;// 遍历HashMap数组上的每个位置if ((e = oldTab[j]) != null) {oldTab[j] = null;// 如果数组的该位置没有元素if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 如果数组的该位置为红黑树else if (e instanceof TreeNode)((TreeNode)e).split(this, newTab, j, oldCap);// 如果数组的该位置是链表!!!!!!,就要尝试把他们分开!!,也就是让数据更加分散// 这里一定要看,我没有把这部分放在正文,如果放在正文你可能翻着看源码很麻烦// 先讲一讲下面的步骤,定义两个链表,lowHead低链表,hiHead高链表.lowHead存储 e.hash & oldCap == 0 的元素,也就是链表上的数据,如果其 hash & oldCap 就追加到lowHead链表上// 同理,如果链表上的元素 e.hash & oldCap == 1,那么就追加到hiHead上面// 最后 低链表存储到原位置,而高链表放到 原位置 + oldCap上// 提示,你可以看下面的正文了else { // preserve order// Node loHead = null, loTail = null;Node hiHead = null, hiTail = null;Node next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null); // 别忘记这里有一个do-while语句遍历链表中的元素啊,这样才能判断链表的每个元素if (loTail != null) {loTail.next = null;newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;newTab[j + oldCap] = hiHead;}}}}}return newTab;}

开始分析啦:
为什么需要把原来产生 hash 冲突的数据, 还要分开呢?
为什么分开是对 e.hash & oldCap 呢??

第一个问题: 不知道你有没有看到我上面的解释, HashMap 中的哈希冲突, 是 hash 的后 x 为相同 (x 的长度为 n - 1 的二进制的长度).
下面的推理如果你不看上面的内容你就不懂哦, 首先直接举例吧 ,HashMap 数组长度为 16, 那么比较 hash 后几位呢, 是不是后四位
如果长度为 31 呢, 是不是比较的后五位.
同理, 如果从 15 扩容到 31, 是不是如果要知道数据的位置, 是不是要通过 (n-1) & hash 才能得到, 但是在扩容之后 n 也增大了两倍, 一个元素的存储位置的判断, 此题由比较 hash 的后 4 位变成了比较后 5 位对不对??? (你可以画一画, 然后联系我上面的推断, 数据存储在数组的位置索引取决于 hash 的后 x 位)
<结论就是,从n到扩大到2n的过程后,也就是从比较后x位到x+1的过程!!>
然后就会产生一个问题, 原本只比较后 4 位, 两个元素的后 4 位相同, 此时产生了哈希冲突.
但是扩容之后呢? ,你确定原本后 hash 四位相同的两个元素,后五位一定相同吗?
<要将一个链表的数据尝试分散的原理是:扩容之后,需要多对比 hash 的更高一位,而这一位,如果 e.hash & oldCap == 0 则代表无论比较后四位还是后五位,都不会影响映射的结果,也就是不会影响 (n-1) & hash, 但是如果 == 1 呢?, 是不是再计算索引的时候, 会多比较一个 hash 中的数字, 此时元素的索引 index = (n -1) & hash 就发生变化了, 需要移动, 而移动多少位, 你可以看看下面

上面讲了好多, 再放一块讲可能就乱了. 你看到这, 已经知道了, 为什么扩容后需要去判断 e.hash & oldCap 了, 如果结果 == 0 的元素, 那么就放到原位, 如果结果 == 1 的元素就移动到 <原位置+oldCap上>
来分析为什么要移动 oldCap:
例如: Hash Map 的数组长度为 16
n -1 = 15 ,此时的插入元素, 比较的是 hash 的后 4 位没问题吧
扩容后
n -1 = 31, 此时插入元素比较的是 hash 的后 5 位没问题吧.
而此时获取元素, 也是 (n-1) & hash 得到的吧, 比较的是后五位得到的, 而原先需要移动 oldCap 的元素有什么特征?
— 没扩容前, 需要比较后四位, 扩容后呢? 比较后五位
然后看看两者的 & 运算得到的元素索引产生什么区别
1111
… 省略 24 位 00001111 == 15

                       11111

… 省略 24 位 00001111 == 15 +
分析: 扩容后, 再 & 并运算中, 多了一个 1, 也就是说扩容后该元素正确位置应该是 <原索引位置 + 10000 & hash> 即可, 这个 10000 的最高位 1 代表的就是扩容后长度 32-1 == 31 的二进制最高位, 但是又仔细想想, 31 的最高位难道不是和 16 的二进制最高位一样吗???

拓展:
rehash 是啥?, 很多博主都是认为 rehash 是进行重新计算哈希, 然后重新定位到一个新的位置上. 也就是我上面说的过程, 但是你看过之后, 真的是重新计算哈希吗?
只是计算索引时, (n -1) & hash 会多比较一位, hash 值并没有重算, 并且并且啊,hash 是存在于 Node 中的, 根本就需要重新 hash
rehash 应该说是 reindex ()

static class Node implements Map.Entry {final int hash;final K key;V value;Node next;Node(int hash, K key, V value, Node next) {this.hash = hash;this.key = key;this.value = value;this.next = next;}

总结一下文章的内容吧

  • HashMap 中的 hash 是由 hashCode 经过 hashcode ^ (hash >>> 16)得到的, 而元素的存储位置是由 (n-1) & hash 得到的
  • Hash Map 中的哈希冲突指的是在当前容量下, hash 的后 x 位相同, 即认定为 HashMap 中的哈希冲突,x == 数组长度-1 的二进制长度;
  • (易错)很多博主认为 HashMap 中产生哈希冲突的两个元素是 hashCode 相同或者是 hash 相同, 其实都不对啊, 我看了无数的博客, 大部分博客都是这么写的
  • 在 HashMap 中, hash 冲突指的不是两个元素 hash 值完全相同
  • rehash ()的含义应该是 reinde

x ())

相关内容

热门资讯

122.(leaflet篇)l... 听老人家说:多看美女会长寿 地图之家总目录(订阅之前建议先查看该博客) 文章末尾处提供保证可运行...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
育碧GDC2018程序化大世界... 1.传统手动绘制森林的问题 采用手动绘制的方法的话,每次迭代地形都要手动再绘制森林。这...
Vue使用pdf-lib为文件... 之前也写过两篇预览pdf的,但是没有加水印,这是链接:Vu...
PyQt5数据库开发1 4.1... 文章目录 前言 步骤/方法 1 使用windows身份登录 2 启用混合登录模式 3 允许远程连接服...
Android studio ... 解决 Android studio 出现“The emulator process for AVD ...
Linux基础命令大全(上) ♥️作者:小刘在C站 ♥️个人主页:小刘主页 ♥️每天分享云计算网络运维...
再谈解决“因为文件包含病毒或潜... 前面出了一篇博文专门来解决“因为文件包含病毒或潜在的垃圾软件”的问题,其中第二种方法有...
南京邮电大学通达学院2023c... 题目展示 一.问题描述 实验题目1 定义一个学生类,其中包括如下内容: (1)私有数据成员 ①年龄 ...
PageObject 六大原则 PageObject六大原则: 1.封装服务的方法 2.不要暴露页面的细节 3.通过r...
【Linux网络编程】01:S... Socket多进程 OVERVIEWSocket多进程1.Server2.Client3.bug&...
数据结构刷题(二十五):122... 1.122. 买卖股票的最佳时机 II思路:贪心。把利润分解为每天为单位的维度,然后收...
浏览器事件循环 事件循环 浏览器的进程模型 何为进程? 程序运行需要有它自己专属的内存空间࿰...
8个免费图片/照片压缩工具帮您... 继续查看一些最好的图像压缩工具,以提升用户体验和存储空间以及网站使用支持。 无数图像压...
计算机二级Python备考(2... 目录  一、选择题 1.在Python语言中: 2.知识点 二、基本操作题 1. j...
端电压 相电压 线电压 记得刚接触矢量控制的时候,拿到板子,就赶紧去测各种波形,结...
如何使用Python检测和识别... 车牌检测与识别技术用途广泛,可以用于道路系统、无票停车场、车辆门禁等。这项技术结合了计...
带环链表详解 目录 一、什么是环形链表 二、判断是否为环形链表 2.1 具体题目 2.2 具体思路 2.3 思路的...
【C语言进阶:刨根究底字符串函... 本节重点内容: 深入理解strcpy函数的使用学会strcpy函数的模拟实现⚡strc...
Django web开发(一)... 文章目录前端开发1.快速开发网站2.标签2.1 编码2.2 title2.3 标题2.4 div和s...