乐趣区

关于java:华为面试官为什么HashMap的加载因子是075

IT 老哥

老哥是通过自学进入大厂做资深 Java 工程师,每天分享技术干货,助你进大厂

有很多货色之前在学的时候没怎么留神,笔者也是在重温 HashMap 的时候发现有很多能够去细究的问题,最终是会回归于数学的,如 HashMap 的加载因子为什么是 0.75?

本文次要对以下内容进行介绍:

  • 为什么 HashMap 须要加载因子?

  • 解决抵触有什么办法?

  • 为什么加载因子肯定是 0.75?而不是 0.8,0.6?

为什么 HashMap 须要加载因子?

HashMap 的底层是哈希表,是存储键值对的构造类型,它须要通过肯定的计算才能够确定数据在哈希表中的存储地位:

`static final int hash(Object key) {`
 `int h;`
 `return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);`
`}`
`// AbstractMap`
`public int hashCode() {`
 `int h = 0;`
 `Iterator<Entry<K,V>> i = entrySet().iterator();`
 `while (i.hasNext())`
 `h += i.next().hashCode();`
 `return h;`
`}`

个别的数据结构,不是查问快就是插入快,HashMap 就是一个插入慢、查问快的数据结构。

但这种数据结构容易产生两种问题:① 如果空间利用率高,那么通过的哈希算法计算存储地位的时候,会发现很多存储地位曾经有数据了(哈希抵触);② 如果为了防止产生哈希抵触,增大数组容量,就会导致空间利用率不高。

而加载因子就是示意 Hash 表中元素的填满水平。

加载因子 = 填入表中的元素个数 / 散列表的长度

加载因子越大,填满的元素越多,空间利用率越高,但发生冲突的机会变大了;

加载因子越小,填满的元素越少,抵触产生的机会减小,但空间节约了更多了,而且还会进步扩容 rehash 操作的次数。

抵触的机会越大,阐明须要查找的数据还须要通过另一个路径查找,这样查找的老本就越高。因而,必须在“抵触的机会”与“空间利用率”之间,寻找一种均衡与折衷。

所以咱们也能晓得,影响查找效率的因素次要有这几种:

  • 散列函数是否能够将哈希表中的数据平均地散列?

  • 怎么解决抵触?

  • 哈希表的加载因子怎么抉择?

本文次要对后两个问题进行介绍。

解决抵触有什么办法?

1. 凋谢定址法

`Hi = (H(key) + di) MOD m,其中 i =1,2,…,k(k<=m-1)`

H(key)为哈希函数,m 为哈希表表长,di 为增量序列,i 为已发生冲突的次数。其中,凋谢定址法依据步长不同能够分为 3 种:

1.1 线性探查法(Linear Probing):di = 1,2,3,…,m-1

简略地说,就是以以后抵触地位为终点,步长为 1 循环查找,直到找到一个空的地位,如果循环完了都占不到地位,就阐明容器曾经满了。举个栗子,就像你在饭点去街上吃饭,挨家去看是否有地位一样。

1.2 平方探测法(Quadratic Probing):di = ±12, ±22,±32,…,±k2(k≤m/2)

绝对于线性探查法,这就相当于的步长为 di = i2 来循环查找,直到找到空的地位。以下面那个例子来看,当初你不是挨家去看有没有地位了,而是拿手机算去第 i2 家店,而后去问这家店有没有地位。

1.3 伪随机探测法:di = 伪随机数序列

这个就是取随机数来作为步长。还是用下面的例子,这次就是齐全按情绪去选一家店问有没有地位了。

但凋谢定址法有这些毛病:

  • 这种办法建设起来的哈希表,当抵触多的时候数据容易堆集在一起,这时候对查找不敌对;

  • 删除结点的时候不能简略将结点的空间置空,否则将截断在它填入散列表之后的同义词结点查找门路。因而如果要删除结点,只能在被删结点上增加删除标记,而不能真正删除结点;

  • 如果哈希表的空间曾经满了,还须要建设一个溢出表,来存入多进去的元素。

2. 再哈希法

`Hi = RHi(key), 其中 i =1,2,…,k`

RHi()函数是不同于 H()的哈希函数,用于同义词产生地址抵触时,计算出另一个哈希函数地址,直到不发生冲突地位。这种办法不容易产生堆集,然而会减少计算工夫。

所以再哈希法的毛病是:减少了计算工夫。

3. 建设一个公共溢出区

假如哈希函数的值域为 [0, m-1],设向量 HashTable[0,…,m-1] 为根本表,每个重量寄存一个记录,另外还设置了向量 OverTable[0,…,v]为溢出表。根本表中存储的是关键字的记录,一旦发生冲突,不论他们哈希函数失去的哈希地址是什么,都填入溢出表。

但这个办法的毛病在于:查找抵触数据的时候,须要遍历溢出表能力失去数据。

4. 链地址法(拉链法)

将抵触地位的元素结构成链表。在增加数据的时候,如果哈希地址与哈希表上的元素抵触,就放在这个地位的链表上。

拉链法的长处:

  • 解决抵触的形式简略,且无堆集景象,非同义词绝不会发生冲突,因而均匀查找长度较短;

  • 因为拉链法中各链表上的结点空间是动静申请的,所以它更适宜造表前无奈确定表长的状况;

  • 删除结点操作易于实现,只有简略地删除链表上的相应的结点即可。

拉链法的毛病:须要额定的存储空间。

从 HashMap 的底层构造中咱们能够看到,HashMap 采纳是数组 + 链表 / 红黑树的组合来作为底层构造,也就是凋谢地址法 + 链地址法的形式来实现 HashMap。

为什么 HashMap 加载因子肯定是 0.75?而不是 0.8,0.6?

从上文咱们晓得,HashMap 的底层其实也是哈希表(散列表),而解决抵触的形式是链地址法。HashMap 的初始容量大小默认是 16,为了缩小抵触产生的概率,当 HashMap 的数组长度达到一个临界值的时候,就会触发扩容,把所有元素 rehash 之后再放在扩容后的容器中,这是一个相当耗时的操作。

而这个临界值就是由加载因子和以后容器的容量大小来确定的:

临界值 = DEFAULT\_INITIAL\_CAPACITY * DEFAULT\_LOAD\_FACTOR

即默认状况下是 16×0.75=12 时,就会触发扩容操作。

那么为什么抉择了 0.75 作为 HashMap 的加载因子呢?这个跟一个统计学里很重要的原理——泊松散布无关。

泊松散布是统计学和概率学常见的离散概率分布,实用于形容单位工夫内随机事件产生的次数的概率分布。有趣味的读者能够看看维基百科或者阮一峰老师的这篇文章:泊松散布和指数分布:10 分钟教程[1]

等号的右边,P 示意概率,N 示意某种函数关系,t 示意工夫,n 示意数量。等号的左边,λ 示意事件的频率。

在 HashMap 的源码中有这么一段正文:

`* Ideally, under random hashCodes, the frequency of`
`* nodes in bins follows a Poisson distribution`
`* (http://en.wikipedia.org/wiki/Poisson_distribution) with a`
`* parameter of about 0.5 on average for the default resizing`
`* threshold of 0.75, although with a large variance because of`
`* resizing granularity. Ignoring variance, the expected`
`* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /`
`* factorial(k)). The first values are:`
`* 0:    0.60653066`
`* 1:    0.30326533`
`* 2:    0.07581633`
`* 3:    0.01263606`
`* 4:    0.00157952`
`* 5:    0.00015795`
`* 6:    0.00001316`
`* 7:    0.00000094`
`* 8:    0.00000006`
`* more: less than 1 in ten million`

在现实状况下,应用随机哈希码,在扩容阈值(加载因子)为 0.75 的状况下,节点呈现在频率在 Hash 桶(表)中遵循参数均匀为 0.5 的泊松散布。疏忽方差,即 X = λt,P(λt = k),其中 λt = 0.5 的状况,按公式:

计算结果如上述的列表所示,当一个 bin 中的链表长度达到 8 个元素的时候,概率为 0.00000006,简直是一个不可能事件。

所以咱们能够晓得,其实常数 0.5 是作为参数代入泊松散布来计算的,而加载因子 0.75 是作为一个条件,当 HashMap 长度为 length/size ≥ 0.75 时就扩容,在这个条件下,抵触后的拉链长度和概率后果为:

`0:    0.60653066`
`1:    0.30326533`
`2:    0.07581633`
`3:    0.01263606`
`4:    0.00157952`
`5:    0.00015795`
`6:    0.00001316`
`7:    0.00000094`
`8:    0.00000006`

那么为什么不能够是 0.8 或者 0.6 呢?

HashMap 中除了哈希算法之外,有两个参数影响了性能:初始容量和加载因子。初始容量是哈希表在创立时的容量,加载因子是哈希表在其容量主动扩容之前能够达到多满的一种度量。

在维基百科来形容加载因子:

对于凋谢定址法,加载因子是特地重要因素,应严格限度在 0.7-0.8 以下。超过 0.8,查表时的 CPU 缓存不命中(cache missing)依照指数曲线回升。因而,一些采纳凋谢定址法的 hash 库,如 Java 的零碎库限度了加载因子为 0.75,超过此值将 resize 散列表。

在设置初始容量时应该思考到映射中所需的条目数及其加载因子,以便最大限度地缩小扩容 rehash 操作次数,所以,个别在应用 HashMap 时倡议依据预估值设置初始容量,以便缩小扩容操作。

抉择 0.75 作为默认的加载因子,齐全是工夫和空间老本上寻求的一种折衷抉择。

退出移动版