为啥 HashMap 调整大小以防发生碰撞或最坏的情况
Posted
技术标签:
【中文标题】为啥 HashMap 调整大小以防发生碰撞或最坏的情况【英文标题】:Why HashMap resize In case of collision or worst case为什么 HashMap 调整大小以防发生碰撞或最坏的情况 【发布时间】:2017-11-28 11:20:44 【问题描述】:我只针对 1.7 之前的 java 版本提出这个问题。我正在使用反射来找出 HashMap 的当前容量。在下面的程序中,将 12 个唯一的人放入一个 HashMap 桶中(使用相同的哈希码)。然后我将第 13 个独特的人放在相同或不同的桶上(使用相同或不同的哈希码)。在这两种情况下,添加第 13 个元素后,HashMap 都将大小调整为 32 个桶。我知道由于负载因子 0.75 和初始容量 16 HashMap 调整到其第 13 个元素的两倍。但是仍然有可用的空桶,这第 13 个元素只使用了 2 个桶。
我的问题是:
我的理解是否正确。难道我没有犯任何错误。这是 HashMap 的预期行为吗?
如果这一切都是正确的,那么即使有 12 或 11 个空闲桶,为什么在这种情况下需要将 HashMap 与第 13 个元素加倍。调整 HashMap 的大小不是额外的开销或成本吗?在这种情况下需要将 HashMap 加倍吗?而根据 hashcode 可以将 13th 放入任何可用的桶中?
.
public class HashMapTest
public static void main(String[] args)
throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
HashMap<Person, String> hm = new HashMap<Person, String>();
for (int i = 1; i <= 12; i++)
// 12 Entry in same bucket(linkedlist)
hm.put(new Person(), "1");
System.out.println("Number of Buckets in HashMap : " + bucketCount(hm));
System.out.println("Number of Entry in HashMap : " + hm.size());
System.out.println("**********************************");
// 13th element in different bucket
hm.put(new Person(2), "2");
System.out.println("Number of Buckets in HashMap : " + bucketCount(hm));
System.out.println("Number of Entry in HashMap : " + hm.size());
public static int bucketCount(HashMap<Person, String> h)
throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException
Field tableField = HashMap.class.getDeclaredField("table");
tableField.setAccessible(true);
Object[] table = (Object[]) tableField.get(h);
return table == null ? 0 : table.length;
class Person
int age = 0;
Person()
Person(int a)
age = a;
@Override
public boolean equals(Object obj)
return false;
@Override
public int hashCode()
if (age != 0)
return 1;
else
return age;
输出
Number of Buckets in HashMap : 16
Number of Entry in HashMap : 12
**********************************
Number of Buckets in HashMap : 32
Number of Entry in HashMap : 13
【问题讨论】:
@Am_I_Helpful 对于第二个答案,负载因子适用于条目计数。为什么它看不到已经可用的存储桶。为什么它会调整大小,这会损害性能。 【参考方案1】:是的,您观察到的行为是预期的行为。
HashMap
的实现希望您使用合理的hashCode
作为键。它假定您的hashCode
将在可用存储桶中尽可能均匀地分配密钥。如果您不这样做(就像您在示例中所做的那样 - 所有键都具有相同的 hashCode
),您将获得糟糕的性能。
在均匀分布的假设下,一旦通过负载因子,HashMap
将其大小加倍是有意义的。它不检查实际上有多少桶是空的(因为它无法知道新条目是分配给空桶还是被占用的桶)。它只检查每个桶的平均条目数。一旦该数量超过负载因子,桶的数量就会增加一倍。
【讨论】:
Ean 根据您的回答,您说 因为它无法知道新条目是分配给空桶还是被占用的桶。现在问题 1 。特定存储桶如何链接到 HashCode。我认为首先计算了 HasCode,然后将其附加/链接到空桶。如果我错了,请纠正我。问题2。我认为一个新条目肯定会分配给空桶或已经占用的桶。现在在这两种情况下都不需要将 hashmap 的大小加倍。我错了吗?是的,如果所有存储桶都已满,那么将 HashMap 加倍是有意义的 @PradeepSingh 根据 hashCode 的值(由 HashMap 实现转换以尝试改善分布)和当前的桶数将 hashCode 映射到桶。桶是否已被占用没有区别。 @PradeepSingh 至于你的第二个问题,当具有不同哈希码的键映射到同一个存储桶时,将 HashMap 加倍会有所帮助。当映射加倍时,这些键可能会被分离到不同的桶中,从而减少在这些桶中的搜索时间,因为新桶中的每个条目都比原来的桶少。 无法理解当具有不同哈希码的键映射到同一个桶时。这是怎么发生的?据我所知,不同的哈希码应该映射到同一个桶。 @PradeepSingh 为简单起见,我们假设使用 hashCode % numberOfBuckets 选择存储桶。现在假设桶的数量是 7,hashCodes 是 0、7、14、21 和 28。所有对象最终都会在同一个桶 0 中。现在如果将地图大小调整为 11 个桶,对象最终分别为存储桶 0、0、3、10 和 6。【参考方案2】:-
是的,这是预期的行为。
HashMap 不关心使用了多少桶。它只知道已经达到负载因子,并且发生碰撞的概率因此变得太大,因此应该调整地图的大小。尽管已经发生了许多碰撞,但调整地图大小实际上可以解决这个问题。不是你的情况,因为你故意选择了相同的 hashCode,但在更现实的情况下,hashCodes 应该有更好的分布。如果您故意选择错误的 hashCodes,HashMap 将无法提高自身的效率,并且增加复杂性来处理极端情况是没有意义的,这种情况永远不会发生,而且 HashMap 无论如何也无法修复。
【讨论】:
嗨,JB!我注意到还有 1 件事。由于负载系数 0.75,第 13 个元素的容量变为 32。现在,如果我从 HashMap 中逐个删除元素。容量不会回到 16。甚至 HashMap 现在也只包含 7 个条目。容量还是32。如果条目数下降,HashMap是否需要减少容量? 不,HashMap 会根据需要扩展,但不会收缩。 哦,好的,谢谢 JB :) @Pradeep 如果你想让hashmap收缩回来,你总是可以通过实现Map接口来自定义hashmap的实现。【参考方案3】:这里还有一个小方面;当您调整内部数组的大小(从 16 到 32)时,您也在“触摸”所有条目。让我解释一下:
当有 16 个桶时(内部数组大小为 16),只有 last 4 bits
决定该条目的去向;想想%
,但在内部它实际上是(n - 1) & hash
,其中n
是桶的数量。
当内部数组增长时,会多考虑一位来决定条目的去向:以前是4 bits
,现在是5 bits
;这意味着所有条目都重新散列,它们现在可能会移动到不同的存储桶;这就是发生调整大小以分散条目的原因。
如果您真的想要填补所有“空白”,请指定1
中的load_factor
;而不是默认的0.75
;但这具有 HashMap 构造函数中记录的含义。
【讨论】:
以上是关于为啥 HashMap 调整大小以防发生碰撞或最坏的情况的主要内容,如果未能解决你的问题,请参考以下文章
(递归记忆化)最好与最坏的选择——375. 猜数字大小 II