为什么hashMap的容量扩容时一定是2的幂次

本文深入探讨HashMap中键值对索引计算方法,解析hash算法如何确保元素分布均匀,减少碰撞,提升查询效率。强调数组长度为2的幂次方的重要性。

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

目录

一、HashMap通过哈希算法得出哈希值之后,将键值对放入哪个索引的方法

二、再例如:hashMap源码获取元素的位置

三、根据Hash算法进行观察:


一、HashMap通过哈希算法得出哈希值之后,将键值对放入哪个索引的方法

static int indexFor(int h, int length) {

// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

return h & (length-1);

}

HashMap的容量为16转化成二进制为10000,length-1得出的二进制为01111 

哈希值为1111 

可以得出索引的位置为15

假设 

HashMap的容量为15转化成二进制为1111,length-1得出的二进制为1110 

哈希值为1111和1110 

 

那么两个索引的位置都是14,就会造成分布不均匀了,

增加了碰撞的几率,

减慢了查询的效率,

造成空间的浪费。 

总结:

  1. 因为2的幂-1都是11111结尾的,所以碰撞几率小。

二、再例如:hashMap源码获取元素的位置

static int indexFor(int h, int length) {

// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";

return h & (length-1);

}

解释:

h:为插入元素的hashcode

length:为map的容量大小

&:与操作 比如 1101 & 1011=1001

如果length为2的次幂 则length-1 转化为二进制必定是11111……的形式,在于h的二进制与操作效率会非常的快,

而且空间不浪费;如果length不是2的次幂,比如length为15,则length-1为14,对应的二进制为1110,在于h与操作,

最后一位都为0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费

 PS: 这都是老版本jdk的源码,1.7,8之后都没有这个方法了, 但是计算位置index的思想不变,就是要充分散列,减少碰撞

下面是1.8的代码

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

boolean evict) {

Node<K,V>[] tab; Node<K,V> p; int n, i;

if ((tab = table) == null || (n = tab.length) == 0)

n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null) //计算index

tab[i] = newNode(hash, key, value, null);

三、根据Hash算法进行观察:

我们可以看到在hashmap中要找到某个元素,需要根据key的hash值来求得对应数组中的位置。如何计算这个位置就是hash算法。前面说过hashmap的数据结构是数组和链表的结合,所以我们当然希望这个hashmap里面的元素位置尽量的分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。 

所以我们首先想到的就是把hashcode对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,“模”运算的消耗还是比较大的,能不能找一种更快速,消耗更小的方式那?java中时这样做的,

Java代码  

  1. static int indexFor(int h, int length) {  
  1.        return h & (length-1);  
  1.    }  

首先算得key得hashcode值,然后跟数组的长度-1做一次“与”运算(&)。看上去很简单,其实比较有玄机。比如数组的长度是2的4次方,那么hashcode就会和2的4次方-1做“与”运算。很多人都有这个疑问,为什么hashmap的数组初始化大小都是2的次方大小时,hashmap的效率最高,我以2的4次方举例,来解释一下为什么数组大小为2的幂时hashmap访问的性能最高。 

         看下图,左边两组是数组长度为16(2的4次方),右边两组是数组长度为15。两组的hashcode均为8和9,但是很明显,当它们和1110“与”的时候,产生了相同的结果,也就是说它们会定位到数组中的同一个位置上去,这就产生了碰撞,8和9会被放到同一个链表上,那么查询的时候就需要遍历这个链表,得到8或者9,这样就降低了查询的效率。同时,我们也可以发现,当数组长度为15的时候,hashcode的值会与14(1110)进行“与”,那么最后一位永远是0,而0001,0011,0101,1001,1011,0111,1101这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率! 

 

 

         所以说,当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 

        说到这里,我们再回头看一下hashmap中默认的数组大小是多少,查看源代码可以得知是16,为什么是16,而不是15,也不是20呢,看到上面annegu的解释之后我们就清楚了吧,显然是因为16是2的整数次幂的原因,在小数据量的情况下16比15和20更能减少key之间的碰撞,而加快查询的效率。 

所以,在存储大容量数据的时候,最好预先指定hashmap的size为2的整数次幂次方。就算不指定的话,也会以大于且最接近指定值大小的2次幂来初始化的,代码如下(HashMap的构造方法中):

Java代码  

  1. // Find a power of 2 >= initialCapacity  
  1.         int capacity = 1;  
  1.         while (capacity < initialCapacity)   
  1.             capacity <<= 1;  

总结: 

        本文主要描述了HashMap的结构,和hashmap中hash函数的实现,以及该实现的特性,同时描述了hashmap中resize带来性能消耗的根本原因,以及将普通的域模型对象作为key的基本要求。尤其是hash函数的实现,可以说是整个HashMap的精髓所在,只有真正理解了这个hash函数,才可以说对HashMap有了一定的理解。

该博文用于扩展学习 用于Java集合深度解析之全面解析HashMap

转载:https://blog.csdn.net/sd_csdn_scy/article/details/57083619

          https://www.cnblogs.com/wengshuhang/

          https://blog.csdn.net/weixin_36910300/article/details/79538421

<think>嗯,我现在要回答用户的问题:“hashmap扩容都是2倍吗”。首先,我得确定用户指的是哪种编程语言中的HashMap实现,因为不同语言的实现可能不同。常见的如Java、Python、C++等,它们的HashMap或类似结构可能有不同的扩容策略。但用户没有特别指明,所以可能需要先说明这一点,再分别讨论不同语言的情况。 首先,我应该回想自己知道的Java中的HashMap。Java的HashMap扩容确实是按照2倍的旧容量扩容的。这是因为Java的HashMap使用了一个基于数组的链表(或者红黑树,当链表过长),而容量总是保持为2幂次方。这样做的好处是在计算元素的存储位置,可以通过位运算((n-1) & hash)来替代取模运算,提高效率。每扩容容量翻倍,这样元素的位置要么保持不变,要么在原位置加上旧容量。这有助于减少重新计算索引的开销。 不过,这里可能需要确认一下具体的Java版本是否有变化。比如,Java 8之后HashMap的实现是否有调整?根据我所知,Java 8及以后版本虽然引入了红黑树优化链表过长的问题,但扩容机制依然是翻倍,所以这个点应该没错。 接下来是其他语言的情况。比如,Python中的字典(dict)是否也是两倍扩容?我记得Python的字典实现采用了更复杂的策略,可能不是严格的2倍。根据资料,Python的dict在扩容会增加当前容量的约1/3到当前容量,使得总容量大约是原来的约1.625倍(具体数值可能因版本而异)。例如,当当前容量超过2/3,会触发扩容,新容量是现有键值对数量的大约4倍,但可能这个比例会调整。不过需要查证准确的数据,可能Python的扩容策略不是固定的倍数,而是根据使用情况动态调整,但可能不是严格的2倍。 再比如,C++中的std::unordered_map,它的扩容机制可能由负载因子触发,但具体的增长倍数可能由实现决定,标准并没有规定必须是2倍。通常实现可能会选择类似2倍的策略,但这也取决于库的实现,比如GCC的STL和MSVC的可能不同,所以不能一概而论。 另外,像Go语言中的map,扩容策略可能分为两种:增量扩容和等量扩容,具体扩容的倍数可能不是固定的2倍,而是根据当前bucket的使用情况来决定。比如,当负载因子超过一定阈值,会进行扩容,可能增加一倍或者重新组织buckets。 所以,总结起来,用户的问题需要分情况讨论。不同编程语言的HashMap实现可能有不同的扩容策略,而最常见的Java中的HashMap确实是每扩容为原来的2倍,但其他语言可能不同。因此,回答的候需要明确指出这点,并给出具体语言的例子。 另外,可能需要解释为什么Java选择2扩容。这可能与保持容量2幂次方有关,从而优化索引的计算。同扩容后元素的位置变化规律,减少rehash的计算量,这也是设计上的考虑。 在回答,还要注意用户的可能需求。用户可能是在学习Java的HashMap,或者在比较不同数据结构的实现,或者遇到了性能问题需要了解扩容机制。因此,提供不同语言的情况,并强调具体情况取决于实现,这样可以帮助用户更全面地理解问题。 需要检查是否有其他语言的HashMap扩容策略是2倍的情况。比如,JavaScript的Map对象,它的内部实现可能由不同的JavaScript引擎决定,如V8引擎可能采用不同的策略,但我不确定具体是否是2倍,可能需要说明这点不确定,但一般来说,常见的如Java是明确的。 所以,最终的答案结构应该是:先指出不同语言实现不同,然后重点说明Java的情况,再简要提及其他语言的可能差异,最后总结回答用户的问题,即不一定是所有情况都是两倍,而是视具体实现而定,Java是的。</think>在回答这个问题,需要区分不同编程语言中`HashMap`(或类似结构)的具体实现。以下是分步解释: --- ### 1. **核心结论** **不一定所有`HashMap`的扩容都是2倍**,具体取决于编程语言和实现细节。最常见的Java `HashMap`确实按2扩容,但其他语言(如Python、C++等)的实现可能不同。 --- ### 2. **Java的HashMap:严格2扩容** - **机制**: - Java的`HashMap`默认初始容量为16,负载因子为0.75(触发扩容的阈值 = 容量 × 负载因子)。 - 每扩容,新容量为旧容量的**2倍**。 - **原因**: - 保持容量为**2幂次方**(如16 → 32 → 64),以便通过位运算 `(n-1) & hash` 快速计算索引。 - 扩容后,元素的新位置只有两种可能:原位置或原位置 + 旧容量,减少了重新哈希的开销。 - **验证**: ```java HashMap<Integer, String> map = new HashMap<>(); map.put(1, "A"); // 初始容量16,当元素数超过12(16×0.75)扩容至32。 ``` --- ### 3. **其他语言的实现差异** #### **Python的字典(dict)** - **机制**: - 采用更复杂的动态扩容策略,容量增长公式为 `new_size ≈ used_slots × 4`,但具体数值可能因版本而异。 - 目标是保持哈希表稀疏(负载因子通常小于2/3),并非严格2倍。 - **示例**: - 若当前容量为8,当插入第6个元素(8×2/3 ≈ 5.33),可能扩容至16。 #### **C++的std::unordered_map** - **机制**: - 标准未规定具体扩容策略,但通常实现(如GCC的STL)按“近似2倍”扩容。 - 触发条件由负载因子(默认1.0)控制,扩容容量可能变为下一个素数(非严格2倍)。 #### **Go的map** - **机制**: - 采用增量扩容(双倍扩容)和等量扩容(重新排列buckets)两种策略。 - 双倍扩容容量增长为旧容量2倍。 --- ### 4. **为什么Java选择2扩容?** - **性能优化**: - 位运算替代取模运算:`index = (n-1) & hash` 比 `hash % n` 更快。 - 扩容元素迁移更高效(只需判断高位是否为1)。 - **空间利用**: - 保持容量2幂次方,减少哈希冲突。 --- ### 5. **总结** - **Java的HashMap**:严格按2扩容。 - **其他语言**:可能采用不同策略(如Python动态调整、C++选择素数等)。 - **关键点**:扩容策略与语言设计目标(性能、内存效率)密切相关。 --- 如果你在特定语言中遇到性能问题,可以进一步分析其`HashMap`实现细节!
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值