本文参考:
https://blog.csdn.net/yyyljw/article/details/80903391
https://www.jianshu.com/p/a89e9487a06c
https://blog.csdn.net/woshimaxiao1/article/details/83661464
https://blog.csdn.net/eaphyy/article/details/84386313
https://blog.csdn.net/qq_33666602/article/details/80139620
所学浅薄,抛砖引玉。
这里会涉及:
1. 哈希表
2.HashMap的最大容量为什么是2的30次方
3. HashMap 的hash算法
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
4. HashMap 查找数组下标为什么是
(n - 1) & hash
哈希表
散列表(也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
来让我们在研究哈希表之前,先看下其他数据结构
数组:是在内存中存储相同数据类型的物理上连续的空间。特点:查找快,插入、删除等更新操作慢。
线性链表:在链式存储结构中,存储数据结构的存储空间可以不连续,各数据结点的存储顺序与数据元素之间的逻辑关系可以不一致,而数据元素之间的逻辑关系是由指针域来确定的。特点:查找慢,插入、删除等更新操作快。
那么,是否可以设计一种数据结构,结合数组和链表?这就是哈希表。
哈希表特征:
1. 哈希表的存储是key、value形式的。
2. 哈希表的主干就是数组,数组的下标是通过函数确定的,即:地址index=f(key),这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。
3. 数组的元素是链表形式
例子:
如何存储字典里的所有单词能够让搜索、更新的效率快?
这个例子哈希表的解决方案,参见:https://www.cnblogs.com/bloodthirsty/p/12017077.html
HashMap的最大容量为什么是2的30次方
/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30;
void resize(int newCapacity) { Entry[] oldTable = table; int oldCapacity = oldTable.length; if (oldCapacity == MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return; } Entry[] newTable = new Entry[newCapacity]; transfer(newTable, initHashSeedAsNeeded(newCapacity)); table = newTable; threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1); }
可以看到,当容量达到2的30次方时,阈值设置为整形的最大值,不再扩容。
首先看下,1 << 30 含义:
以一个字节为例:1<<2 = 4
0000 0001(十进制1)
向左移动两位之后变成
0000 0100(十进制4)
可见 1<<30 等同于十进制中2的30次幂。
回到问题,为什么是2的30次幂?
因为java中的int类型为4个字节32位二进制,那么按理最大容量应该可以达到2的31次幂。但是事实上二进制中最高位即最左边的一位是符号位(1代表正,0代表负),
所以最大容量为2的30次幂。
解析HashMap 的hash算法
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
1. key如果为0,hash值为0
2. key不为0的情况:
^按位异或运算,不同为1,相同为0;
0^0=0, 1^0=1, 0^1=1, 1^1=0,由此可见,A ^ 0 = A
>>> 无符号右移:高位补0
做个测试
h = key.hashCode() 1111 1101 1101 1111 1000 0101 0011 1100 ^ h >>> 16 0000 0000 0000 0000 1111 1101 1101 1111 右移16位,即保留高16位移动到低位,高16位0补齐 ------------------------------------------------------------------ 1111 1101 1101 1111 0111 1001 1110 0011 做异或,不同则为1,相同则为0
结果可见:高位不变,然后把高16位和低16位做异或
这样做法的意义是什么?
我们知道,HashMap设计中,槽位的计算公式为:hash &(n-1),来继续看:
hash 1111 1101 1101 1111 0111 1001 1110 0011 & 16-1 0000 0000 0000 0000 0000 0000 0000 1111 ------------------------------------------------------------------ 0000 0000 0000 0000 0000 0000 0000 0011 任何数与0做'与'运算都为0
可以看出,在容量为16的情况下,决定槽位的是低四位,hash值得高位不会起作用,所以拿key的hashcode的高16位与低16位做'异或',高位的特性也显现出来了!
为什么是做异或?
异或的特点:0^0=0, 1^0=1, 0^1=1, 1^1=0,覆盖数据均衡。
与运算:0&0=0, 0&1=0, 1&0=0, 1&1=1,向0靠拢了
或运算:0|0=0, 0|1=1, 1|0=1, 1|1=1,向1靠拢了
HashMap 数组槽位为什么是2的N次幂
1.直观枚举
当n为2的幂次方时,(n-1)& hash 的值是均匀分布的,我们假设n=16,hash从0开始递增:
hash (n-1)& hash 结果 0 1111 & 0 0 1 1111 & 1 1 2 1111 & 10 2 3 1111 & 11 3 4 1111 & 100 4 5 1111 & 101 5 …… …… …… 16 1111 & 10000 0 哈希碰撞 17 1111 & 10001 1 哈希碰撞 18 1111 & 10010 2 哈希碰撞
当n不为2的幂次方时,(n-1)& hash 的值不是是均匀分布的,我们假设n=15,hash从0开始递增:
hash (n-1)& hash 结果 0 1110 & 0 0 1 1110 & 1 0 哈希碰撞 2 1110 & 10 2 3 1110 & 11 2 哈希碰撞 4 1110 & 100 4 5 1110 & 101 4 哈希碰撞 …… …… …… 16 1110 & 10000 0 哈希碰撞 17 1110 & 10001 0 哈希碰撞 18 1110 & 10010 2 哈希碰撞
2. 理论
其实,当n=16时,(n-1)& hash = hash % n,这就是为什么在n=16的时候,hash为0~15根本不会发生碰撞。