HashMap扩容过程是什么?怎么解决哈希冲突?
hello啊,各位观众姥爷们!!!本baby今天又来报道了!哈哈哈哈哈嗝🐶
2025 面试题大全🔗
HashMap 如何解决哈希冲突?
哈希冲突是指两个或多个不同的键(key)通过哈希函数计算出了相同的数组下标。
HashMap
主要使用以下两种方法相结合来解决哈希冲突:
1. 链地址法
这是 HashMap
解决冲突的基本方法。
- 原理:数组的每一个元素不是一个单独的值,而是一个"桶",可以存放多个元素。当发生哈希冲突时,将哈希值相同的键值对放入同一个桶中,并以链表的形式链接起来。
- 过程:
- 计算键的哈希值,找到对应的数组下标。
- 如果该位置为空,直接放入新节点。
- 如果该位置不为空(即发生冲突),则遍历这个链表:
- 如果发现某个节点的 Key 与要插入的 Key 相同(通过
equals
方法判断),则更新其 Value。 - 如果没有相同的 Key,则将新节点插入到链表的末尾(JDK 1.7是头插法,JDK 1.8及以后改为尾插法,主要是为了避免并发环境下扩容时产生死循环)。
- 如果发现某个节点的 Key 与要插入的 Key 相同(通过
2. 红黑树优化
在 JDK 8 之后,HashMap
对链地址法进行了重大优化,引入了红黑树。
- 背景:当链表变得非常长时,在链表中查找一个元素的时间复杂度会退化为 O(n),效率很低。
- 优化:当一个桶中的链表长度超过一定阈值(默认为 8)并且 当前数组的长度达到一定阈值(默认为 64)时,链表会自动转换为红黑树。
- 目的:将查找、插入和删除操作的时间复杂度从 O(n) 优化到 O(log n),大大提升了性能。
- 退化:同样,当红黑树中的节点数量由于删除操作而减少到另一个阈值(默认为 6)时,为了节省空间,红黑树会退化为链表。
总结解决哈希冲突的流程:
计算哈希 -> 定位桶 -> 若冲突,则在链表/红黑树中查找 -> 找到则更新,未找到则插入。
HashMap 的扩容过程
扩容(Rehashing)是 HashMap
保持高效性能的关键机制。当元素数量过多,导致哈希冲突概率急剧增加时,通过扩容来减少冲突。
1. 为什么要扩容?
- 降低哈希冲突:数组长度越大,哈希值分布越分散,碰撞几率越低。
- 维持高效性:如果链表过长或树化过多,会严重影响
HashMap
的性能。扩容可以有效减少每个桶中元素的数量。
2. 什么时候触发扩容?
当 HashMap
中的元素数量(size
)超过 容量(capacity) 负载因子(loadFactor)* 时,就会触发扩容。
- 容量:底层数组的长度,默认是 16。
- 负载因子:一个比例系数,默认是 0.75。
- 阈值:
threshold = capacity * loadFactor
。默认情况下,threshold = 16 * 0.75 = 12
。
所以,当元素数量超过 12 时,就会触发第一次扩容。
3. 扩容的具体步骤是什么?
扩容过程可以概括为:创建新数组 -> 重新哈希 -> 迁移数据。
-
创建新数组:
- 创建一个新的数组,其容量是旧数组的 2 倍(即
newCapacity = oldCapacity << 1
)。例如,从 16 扩容到 32。
- 创建一个新的数组,其容量是旧数组的 2 倍(即
-
重新哈希与数据迁移:
- 遍历旧数组中的每一个桶(bucket)。
- 对于每个桶中的每一个节点(可能是链表节点,也可能是树节点):
a. 计算新下标:根据节点的 Key 的哈希值和新数组的长度重新计算它在新数组中的下标。
b. 关键优化:由于新容量是旧容量的 2 倍,所以每个节点在新数组中的位置要么保持不变,要么是原位置 + 旧容量
。- 原因:计算下标的公式是
hash & (length - 1)
。扩容后length-1
的二进制比原来多了一个1
(例如从01111
(15) 变成11111
(31))。这个多出来的1
位与哈希值进行&
操作,结果只能是 0 或 1。如果是 0,则新下标等于原下标;如果是 1,则新下标等于原下标 + 旧容量
。
c. 放置到新数组: - 对于链表:JDK 8 做了一个巧妙的优化。它会将整个链表拆分成两个子链表:一个放在新数组的原索引位置(
loHead
),另一个放在原索引 + 旧容量
的位置(hiHead
)。这样可以保证链表的相对顺序,并且避免了在并发环境下形成死循环(JDK 7 的头插法会有这个问题)。 - 对于红黑树:当拆分一个树节点时,它同样会被拆分成两个链表。然后会检查拆分后的链表长度:
- 如果长度
<= 6
,则树退化为链表。 - 如果长度
> 6
,则将链表重新树化,形成一个新的红黑树。
- 如果长度
- 原因:计算下标的公式是
-
更新引用:
- 将
HashMap
内部的table
引用指向新创建的数组。 - 更新新的扩容阈值
threshold = newCapacity * loadFactor
。
- 将
要点
特性 | 解决方案/过程 |
---|---|
解决哈希冲突 | 链地址法 为主,红黑树 优化长链表。 |
链表转树条件 | 链表长度 > 8 且 数组长度 >= 64。 |
树退化为链表 | 树节点数 <= 6。 |
扩容触发条件 | 元素数量 > 容量 * 负载因子 (默认 16*0.75=12)。 |
扩容后大小 | 原数组长度的 2 倍。 |
扩容核心过程 | 1. 创建2倍大小新数组。 2. 遍历旧数组,对每个节点重新计算下标( 原位置 或 原位置+旧容量 )。3. 将节点迁移到新数组。 |
2025 面试题大全🔗