为什么 HashMap 默认加载因子非得是0.75?

本文解析HashMap加载因子为0.75的科学依据,探讨开放定址法与拉链法解决哈希冲突的方法,以及泊松分布如何影响冲突概率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

点击“开发者技术前线”,选择“星标????”

在看|星标|留言,  真爱

来源:blog.youkuaiyun.com/NYfor2017/article/details/105454097

文前调侃:黄金比例是0.618

前言

有很多东西之前在学的时候没怎么注意,笔者也是在重温HashMap的时候发现有很多可以去细究的问题,最终是会回归于数学的,如HashMap的加载因子为什么是0.75?

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

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

  • 解决冲突有什么方法?

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

(若文章有不正之处,或难以理解的地方,请多多谅解,欢迎指正)

为什么HashMap需要加载因子?

HashMap的底层是哈希表,是存储键值对的结构类型,它需要通过一定的计算才可以确定数据在哈希表中的存储位置:

staticfinalint hash(Object key) {        int h;        return(key == null) ? 0: (h = key.hashCode()) ^ (h >>> 16);    }//AbstractMappublicint 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。

至于为什么在JDK1.8的时候要运用到红黑树,下篇文章会介绍。

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

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

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

临界值 = DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR

即默认情况下是16x0.75=12时,就会触发扩容操作。

那么为什么选择了0.75作为HashMap的加载因子呢?笔者不才,通过看源码解释和大佬的文章,才知道这个跟一个统计学里很重要的原理——泊松分布有关。

泊松分布是统计学和概率学常见的离散概率分布,适用于描述单位时间内随机事件发生的次数的概率分布。有兴趣的读者可以看看维基百科或者阮一峰老师的这篇文章:[泊松分布和指数分布:10分钟教程][10]

等号的左边,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作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择

结语

曾经有一堆高数、线性代数、离散数学摆在我面前,但是我没有珍惜。等到碰到各种数学问题的时候,才后悔莫及。学计算机的时候最痛苦的事,莫过于此。如果老天可以再给我一个,再来一次的机会的话。我会跟当时的我,说三个字——“学数学!”
数学真的太重要。离开大学之后,该怎么学数学啊,有什么好的建议吗?

如果本文对你有帮助,请给一个赞吧,这会是我最大的动力~


福利:

在这里,我为大家准备了一份2020年最新最全的面试题及答案,这套电子书涵盖了诸多后端,客户端,前端技术栈的面试题和答案,相信可以帮助大家在最短的时间内复习的大多数面试题,从而拿到自己心仪的offer。



END

历史推荐面试官问:说说悲观锁、乐观锁、分布式锁?都在什么场景下使用?有什么技巧?面试官问我:用了HTTPS就安全了吗?用了HTTPS会被抓包吗?我竟然回答不上来...美团面试题:String s = new String("111")会创建几个对象?
面试官:POST 比 GET 安全吗?你理解就是错的
点个在看吧
<think>好的,我现在要回答用户关于HashMap默认负载因子为什么0.75的问题。首先,我需要回忆一下HashMap的基本原理,负载因子的作用,以及为什么选择这个数值。 首先,负载因子HashMap在扩容之前允许的填充程度。当元素数量超过容量乘以负载因子时,就会触发扩容。比如默认容量16,负载因子0.75时,当元素超过12个就会扩容到32,依此类推。 接下来,用户想知道为什么选择0.75这个特定值。这可能涉及到时间和空间成本的权衡。负载因子高意味着更少的扩容操作,节省空间,但可能导致哈希冲突增加,查找时间变长。负载因子低则相反,空间占用多,但查找效率高。 我记得Java官方文档中有提到,0.75是在时间和空间成本之间的一种折中选择。可能还有数学上的依据,比如泊松分布或者概率论中的最优值。比如,在理想情况下,哈希冲突的概率较低时,0.75可能是一个平衡点,使得链表长度在大部分情况下保持较短,同时减少频繁的扩容。 另外,可能需要考虑哈希表的性能。比如,当负载因子0.75时,哈希表的平均探测次数在开放地址法中可能达到一个较好的平衡。但JavaHashMap使用的是链表和红黑树(当链表过长时),所以可能这个数值经过测试验证,在大多数场景下性能最优。 此外,可能需要解释负载因子与哈希冲突的关系。较高的负载因子导致更多冲突,而较低的则减少冲突。但扩容是有成本的,所以需要找到一个合适的阈值。 可能需要引用一些资料或官方说明。例如,在JavaHashMap文档中提到,默认负载因子0.75提供了时间和空间效率的良好平衡。较高的值减少了空间开销,但增加了查找成本。而0.75是两者之间的妥协。 总结起来,答案应该包括负载因子的定义,为什么0.75被选中的原因(时间和空间的平衡,数学或经验依据),可能还提到哈希冲突的影响以及扩容的开销。</think><< HashMap的负载因子默认设置为0.75是时间和空间成本之间的折中结果。负载因子(Load Factor)表示哈希表在扩容前允许的填充程度,计算公式为:`元素数量 / 容量`。当元素数量超过`容量 × 负载因子`时,哈希表会触发扩容(容量翻倍,重新哈希)。以下是具体原因: 1. **空间效率与时间效率的平衡** - 负载因子过高(如1.0):虽然减少了内存占用,但会导致哈希冲突概率显著增加,链表或红黑树的查询时间复杂度可能退化到O(n)或O(log n)。 - 负载因子过低(如0.5):虽然减少了哈希冲突,提高了查询效率,但会导致频繁扩容,增加时间和内存的开销。 2. **统计学和数学依据** - 在理想哈希函数下,0.75的负载因子对应的哈希冲突概率较低。根据泊松分布,当负载因子0.75时,哈希表中每个桶(Bucket)的链表长度超过8的概率极小(约千万分之一),适合实际应用场景。 3. **经验验证** - 实际测试表明,0.75在大多数场景下能平衡扩容频率和哈希冲突。例如,默认容量16时,扩容阈值是12(16 × 0.75),在插入第13个元素时触发扩容。 **示例代码**:默认负载因子HashMap构造函数中的体现 ```java HashMap<String, Integer> map = new HashMap<>(); // 默认负载因子0.75 HashMap<String, Integer> customLoadMap = new HashMap<>(16, 0.5f); // 自定义负载因子0.5 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值