HashMap中的hash算法

一、关于Hash表和Hash函数
Hash表也称散列表,直译为哈希表,hash表是一种根据关键字值(key-value)而直接进行访问的数据结构。在哈希表的键值对关系中,key到value中间还存在着一个映射值,这个映射值就是数组的下标index,key正是通过映射到数组对应的下标index而访问到value值的,但key又是如何映射到数组下标的呢?这就要通过一个映射函数f(key),这个函数我们称之为哈希函数

哈希表中,每个key通过哈希函数的计算都会得到一个唯一的index,但并不是每个index都对应一个唯一的key,就是说可能有两个以上的key映射到同一个index,这就产生了哈希冲突的问题,本文讨论的重点不是如何解决哈希冲突,而是基于HashMap的实现来分析一下,如何通过优化hash算法来减少哈希冲突

一个好的hash算法,应该是尽量避免不同的key映射出相同的index,这样才能减少哈希冲突的出现。比如在HashMap中解决哈希冲突采用的是拉链法,这种方法把冲突于某个数组下标的数据都保存到对应数组单元中一个链表中,如下图所示,这种数据结构中数组单元保存不是单一的数值,而是一个链表。按照这种方式,如果哈希冲突越多,可能造成数组的利用率就越低,因为有些数组单元可能被闲置,而数组单元上的链表可能会越大,这势必影响到Map的性能,所以尽可能地避免哈希冲突很重要

当然从另外一个角度来说,哈希冲突是不可避免的,比如一个HashMap的容量为16,现有20个元素要存入到这个Map中,在容量不扩展的情况下,要把20个元素存入容量只有16的HashMap中至少会产生4次哈希冲突,注意了这里是至少4次,但即使无法避免冲突,我们还是要让哈希函数的映射值尽量分散,这样才能尽量减少哈希冲突,提高数组的利用率,避免数组单元的链表过大

二、HashMap的hash算法
来看看HashMap中的hash算法是如何实现的

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
 
//获取index的方法,1.7源码,1.8中用tab[(n - 1) & hash]代替,但原理一样
static int indexFor(int h, int length) {
    return h & (length-1);
}
index的获取很简单,就是把h对 length-1取模,其中length为数组长度,所以关键的是h是怎么计算得到的,也很简单,就是通过 h = (h = key.hashCode()) ^ (h >>> 16)得到的,看似一个简单的公式,其实里面的学问也不小,所以这里的主要疑问是为什么h的计算方法是 h = (h = key.hashCode()) ^ (h >>> 16)

1. h & (length-1)

在公式 h & (length-1)中,其中length为数组的长度,HashMap的数组默认长度是16,因为大多数情况来说数组长度都不会太大,所以影响 h & (length-1)计算结果的主要因素就是h的低位数据了,也就是大多数情况下h的高位数据对计算结果是没有影响的。比如只有当数组长度length大于2^16即大于65536的时候,h的高16位才会对 h & (length-1)的计算结果产生影响,所以在HashMap中我们也主要对h的低16位进行优化,也就是让h的低16位的数据尽量分散,上面的 (h = key.hashCode()) ^ (h >>> 16)算法也是基于这个目的而设计的

2.为什么是(h = key.hashCode()) 和 (h >>> 16)异或

首先 h = key.hashCode()是key对象的一个hashCode,每个不同的对象其哈希值都不相同,其实底层是对象的内存地址的散列值,所以最开始的h是key对应的一个整数类型的哈希值

h >>> 16的意思是将h右移16位,然后高位补0,然后再与(h = key.hashCode()) 异或运算得到最终的h值,为什么是异或运算呢?当然我们知道目的是为了让h的低16位更有散列性,但为什么是异或运算就更有散列性呢?而不是与运算或者或运算呢?网上大多的文章都没有给出一个很好的说明,这里我将证明一下为什么异或就能够得到更好散列性

3.为什么异或运算的散列性更好

先来看一下下面的这组运算

上面是将0110和0101分别进行与、或、异或三种运算得到不同的结果,我们主要来看计算的过程:

与运算:其中1&1=1,其他三种情况1&0=0, 0&0=0, 0&1=0 都等于0,可以看到与运算的结果更多趋向于0,这种散列效果就不好了,运算结果会比较集中在小的值

或运算:其中0&0=0,其他三种情况 1&0=1, 1&1=1, 0&1=1 都等于1,可以看到或运算的结果更多趋向于1,散列效果也不好,运算结果会比较集中在大的值

异或运算:其中0&0=0, 1&1=0,而另外0&1=1, 1&0=1 ,可以看到异或运算结果等于1和0的概率是一样的,这种运算结果出来当然就比较分散均匀了

总的来说,与运算的结果趋向于得到小的值,或运算的结果趋向于得到大的值,异或运算的结果大小值比较均匀分散,这就是我们想要的结果,这也解释了为什么要用异或运算,因为通过异或运算得到的h值会更加分散,进而 h & (length-1)得到的index也会更加分散,哈希冲突也就更少

4.HashMap的容量为什么建议是2的幂次方

2的幂次方是指数组长度length的大小,假如length等于2的幂次方,那样length-1的二进制数据的低位就全部为1了,比如当数组长度为16,那么15的二进制就为1111,只有这样,在计算数组下标index的时候才能更好地利用h的散列性,举个例子:

比如 length-1=15,二进制即为1111,分别跟三个不同的h值进行与运算,计算如下

1111 & 101010100101001001000 结果:1000 = 8

1111 & 101000101101001001001 结果:1001 = 9

1111 & 101010101101101001010 结果:1010 = 10

1111 & 101100100111001101100 结果: 1100 = 12

但是如果length为11的话,那么length-1的二进制则表示为1010,与同样的三个h值与运算,计算如下

1010 & 101010100101001001000 结果:1000 = 8

1010 & 101000101101001001001 结果:1000 = 8

1010 & 101010101101101001010 结果:1010 = 10

1010 & 101100100111001101100 结果: 1000 = 8

很明显,当数组长度为16的时候,没有产生哈希冲突,而为11的时候,产生了3次哈希冲突,所以这就说明了为什么HashMap的容量建议为2的幂次方


版权声明:本文为CSDN博主「 十 月」的原创文章
原文链接:https://blog.csdn.net/liuxingrong666/article/details/103640412

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值