为啥 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) &amp; hash,其中n 是桶的数量。

当内部数组增长时,会多考虑一位来决定条目的去向:以前是4 bits,现在是5 bits;这意味着所有条目都重新散列,它们现在可能会移动到不同的存储桶;这就是发生调整大小以分散条目的原因。

如果您真的想要填补所有“空白”,请指定1 中的load_factor;而不是默认的0.75;但这具有 HashMap 构造函数中记录的含义。

【讨论】:

以上是关于为啥 HashMap 调整大小以防发生碰撞或最坏的情况的主要内容,如果未能解决你的问题,请参考以下文章

(递归记忆化)最好与最坏的选择——375. 猜数字大小 II

为啥 malloc() 基于链表?

李开复曾说:“买车是一生最坏的投资”,真的是这样吗?对此你怎么看?

java集合-补充HashMapJDK1.8

引导列元素调整大小碰撞到新行

Java程序员:这是一个最好的时代,也是一个最坏的时代