HashMap底层实现原理

什么是哈希表

根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

什么是哈希冲突

根据一定的规则放进存放哈希值的数组中,然后下标为1的数组已经有值了,后面根据规则,判定某个数也需要放到下标为1的数组中,这样就导致了只有一个位置两个人都要坐,就引起了冲突。(不同的key值产生的H(key)是一样的)。

解决哈希冲突的方法

1.开放地址

插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。

Hi=(H(key)+di)%m //开放地址法计算下标公式

Hi:下标(储存的地址)

H(key):哈希函数(计算哈希值)

di:增量

%:取模

m:哈希表的长度

探查方法

线性探查

di=1,2,3,…m-1;冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。

二次探查

di=1^2, -1^2, 2^2, -2^2 …k^2, -k^2,(k

随机探查

di=伪随机数序列;冲突发生时,建立一个伪随机数发生器(如i=(i+p) % m),p是质数(在m范围取得质数),生成一个伪随机序列,并给定一个随机数做起点,每次加上伪随机数++就行。

例子

设哈希表长为14,哈希函数为H(key)=key%11。表中现有数据15、38、61和84,其余位置为空,如果用二次探测再散列处理冲突,则49的位置是?使用线性探测法位置是?

解:因为H(key)=key%11

所以15的位置 = 15 % 11=4; 38的位置 = 38 % 11=5; 61的位置 = 61 % 11=6; 84的位置 = 84 % 11=7;(证明哈希表4,5,6,7已经有元素)

因为计算下标的公式为:Hi=(H(key)+di)mod%m

使用二次探测法

H(1) = (49%11 + 1^1) = 6;冲突

H(-1) = (49%11 + (-1^2)) = 4;冲突 注意 -1^2 = -1; (-1)^2 = 1;

H(2) = (49%11 + 2^2) = 9;不冲突

二次探测法49的位置就是哈希表的9。

使用线性探测

H(1) = (49%11 + 1) = 6;冲突

H(2) = (49%11 + 2) = 7;冲突

H(3) = (49%11 + 3) = 8;不冲突

线性探测法49的位置就是哈希表的8。

再哈希法

再哈希法又叫双哈希法,有多个不同的Hash函数,当发生冲突时,使用第二个,第三个,….,等哈希函数计算地址,直到无冲突。虽然不易发生聚集,但是增加了计算时间。

链地址法

每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。

比如 66和88这两个元素哈希值都是1,这就发生了哈希冲突,采用链地址法,可以把 66和88放在同一个链表中。如下图

建立公共溢出区

将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表。

HashMap的hash()算法

为什么不是h = key.hashCode()直接返回,而要 h = key.hashCode() ^ (h >>> 16)来计算哈希值呢?

回答:减少哈希冲突

//源码:计算哈希值的方法 H(key)

static final int hash(Object key) {

int h;

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

}

//^ (异或运算) 相同的二进制数位上,数字相同,结果为0,不同为1。 举例如下:

0 ^ 0 = 0

0 ^ 1 = 1

1 ^ 1 = 0

1 ^ 0 = 1

// &(与运算) 相同的二进制数位上,都是1的时候,结果为1,否则为零。 举例如下:

0 & 0 = 0

0 & 1 = 0

1 & 0 = 0

1 & 1 = 1

h = key.hashCode() ^ (h >>> 16)意思是先获得key的哈希值h,然后 h 和 h右移十六位 做异或运算,运算结果再和 数组长度 - 1 进行 与 运算,计算出储存下标的位置。具体原理如下:

综下所述 储存下标 = 哈希值 & 数组长度 - 1

//jdk1.7中计算数组下标的HashMap源码

static int indexFor(int h, int length) {

//计算储存元素的数组下标

return h & (length-1);

}

//jdk1.8中去掉了indexFor()函数,改为如下

i = (n - 1) & hash //i就是元素存储的数组下标

某个key的哈希值为 :1111 1111 1110 1111 0101 0101 0111 0101,数组初始长度也是16,如果没有 ^ h >>> 16,计算下标如下

1111 1111 1110 1111 0101 0101 0111 0101 //h = hashcode()

& 0000 0000 0000 0000 0000 0000 0000 1111 //数组长度 - 1 = 15 (15的二进制就是 1111)

------------------------------------------

0000 0000 0000 0000 0000 0000 0000 0101 //key的储存下标为5

由上面可知,只相当于取了后面几位进行运算,所以哈希冲突的可能大大增加。

以上条件不变,加上 异或h >>> 16,之后在进行下标计算

1111 1111 1110 1111 0101 0101 0111 0101 //h = hashcode()

^ 0000 0000 0000 0000 1111 1111 1110 1111 //h >>> 16

------------------------------------------

1111 1111 1110 1111 1010 1010 1001 1010 //h = key.hashCode() ^ (h >>> 16)

& 0000 0000 0000 0000 0000 0000 0000 1111 //数组长度 - 1 = 15 (15的二进制就是 1111)

------------------------------------------

0000 0000 0000 0000 0000 0000 0000 1010 //key的存储下标为10

重要:由上可知,因为哈希值得高16位和低16位进行异或运算,混合之后的哈希值,低位也可能掺杂了高位的一部分特性(就是变化性增加了),这样就减少了哈希冲突。

为什么HashMap的初始容量和扩容都是2的次幂

也是为了减少哈希冲突

原理:

因为判断储存位置的下标为 : i = (n - 1) & hash,n就是数组的长度。

2的次幂 - 1,其二进制都是1,比如 2^4 -1= 15(1111),2^5-1 = 31(11111)。

因为 n-1 和 hash 进行与运算,只有 1 & 1 ,才为1。

因为 n-1永远是2的次幂-1,(n - 1) & hash的结果就是 hash的低位的值。

1111 1111 1110 1111 0101 0101 0111 0101 //hash值

& 0000 0000 0000 0000 0000 0000 0000 1111 //数组长度 - 1 = 15 (15的二进制就是 1111)

------------------------------------------

0000 0000 0000 0000 0000 0000 0000 0101 //高位全部清零,只保留末四位(就相当于保留了hash的低位)

如果容量不是2次幂会怎么样呢?如下图表

2次幂的时候,数组长度为16,n-1 = 16 -1 = 15(1111)

  • 非2次幂的时候,数组长度为10,n-1 = 10 -1 = 9(1001)

重要:由上看出,n为2的次幂,哈希冲突会更少,保证元素的均匀插入。

如果指定了不是2的次幂的容量会发生什么

会获得一个大于指定的初始值的最接近2的次幂的值作为初始容量。

比如:输入 9 获得 16,输入 5 获得 8。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值