针对于HashMap的(n-1) & hash的研究和拓展思考
Posted 写Bug的渣渣高
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了针对于HashMap的(n-1) & hash的研究和拓展思考相关的知识,希望对你有一定的参考价值。
本文概览:
简单, 直白的看一看为什么 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<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 注意这里n是hashMap数组的长度
n = (tab = resize()).length;
// 你只需要看这里,插入的时候,使用(n-1) & hash得到一个i
if ((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<K,V>[] resize()
... 前面的源码就不看了
if (oldTab != null)
for (int j = 0; j < oldCap; ++j)
Node<K,V> 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<K,V>)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<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do
next = e.next;
if ((e.hash & oldCap) == 0)
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
else
if (hiTail == null)
hiHead = e;
else
hiTail.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 == 1511111
… 省略 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<K,V> implements Map.Entry<K,V>
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> 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 ())
以上是关于针对于HashMap的(n-1) & hash的研究和拓展思考的主要内容,如果未能解决你的问题,请参考以下文章