【问题标题】:JAVA Hashmap internal implementation - what if bins get too largeJAVA Hashmap 内部实现 - 如果 bin 太大怎么办
【发布时间】:2015-09-28 07:01:33
【问题描述】:

在内部,Hashmaps 使用hashfunction 来查找被查询的key 所属的bin。每个bins 本身就是一个LinkedList

如果这些LinkedLists 可能会变得很长,并且LinkedLists 没有恒定的访问时间,而是线性访问时间,我不明白访问时间如何保持恒定。

即使 bin 由于某种原因变得太大,Java Collections 库如何设法保证恒定的访问时间?内部发生了什么? Java 在内部做了什么来最大程度地减少这种负面影响?

【问题讨论】:

  • 它不做任何保证。 O(1) 性能是最佳情况和平均情况,但前提是您使用的是体面的散列函数和非病态数据集。从 HashMap 中获得 O(n) 性能并非不可能。
  • 内部库是否有某种故障保护 - 某种方式可以最大限度地减少这种危险?
  • 注意:这是一个实现细节。 IBM 可以以不同的方式实现这一点。

标签: java data-structures hashmap


【解决方案1】:

每个 bin 中的平均元素数受一个小常数的约束。这是通过保持 bin 的数量至少与条目总数乘以负载因子(其默认值为 0.75)一样高来维持的。

bin 的数量随着条目数的增加而增加,以保持这种不变性。

这是相关代码(Java 7):

void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

其中size 是条目数,table.length 是 bin 数,thresholdtable.length * loadFactor

如果您使用默认加载因子 0.75(或任何加载因子

【讨论】:

    【解决方案2】:

    documentation tells you,如果负载率过高怎么办:

    HashMap 的实例有两个影响其性能的参数:初始容量和负载因子。容量是哈希表中的桶数,初始容量只是哈希表创建时的容量。负载因子是哈希表在其容量自动增加之前允许达到的程度的度量。 当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表被重新哈希(即内部数据结构被重建),使得哈希表大约有桶数的两倍。

    此外,您可以查看源代码,其中包含相当多的implementation notes 列表。最重要的是:

    此映射通常充当分箱(分桶)哈希表,但当箱变得太大时,它们会转换为 TreeNode 的箱,每个结构类似于 java.util.TreeMap 中的结构。

    还有更多:

    因为 TreeNode 的大小大约是常规节点的两倍,所以我们仅在 bin 包含足够的节点以保证使用时才使用它们(请参阅 TREEIFY_THRESHOLD)。当它们变得太小(由于移除或调整大小)时,它们会被转换回普通 bin。

    简而言之:

    • 当单个 bin 变得太大时,它们的元素会转换为树节点,您会得到 O(ln(t)) 搜索,t 是 bin 的大小。所以 large bins 有一个二叉树悬挂在它们上面。
    • 当整个地图的负载率变高时,bin 的数量会增加一倍,并且整个地图会重新散列(这可能仍会导致某些 bin 再次变成树 bin)。

    【讨论】:

      【解决方案3】:

      如果哈希表太满,则需要重新哈希。为了重新哈希表,创建另一个具有更多桶的表,并将所有元素插入到新表中。原始表被丢弃。

      负载因子决定了何时重新散列。默认值为 0.75,因此当表已满 75% 以上时,会自动使用两倍的桶重新散列。

      为了在表中找到一个位置,计算哈希码并以桶的数量为模减少。这个想法是散列函数应该稍微随机分布对象,所以冲突的数量应该很低,所以不应该有太多的比较。

      【讨论】:

      • 如果哈希表未满,但某个特定 bin 的条目过多,导致遍历需要相当长的时间怎么办?
      • @farid99 见我回答的最后一段
      【解决方案4】:

      如果这些链表变得很长,我不明白访问时间如何保持不变

      HashMap 不提供保证的恒定访问时间。它提供了摊销常数时间,这是另一回事:n 项目的总体访问平均为 O(1),但每个单独的访问可能为 O(n)。

      此外,只有当哈希函数“好”时,才能实现摊销常数时间。当散列函数不好时(例如,返回一个常量,这是一个有效但非常糟糕的散列函数),库就无能为力了:访问时间将是线性的,无论实现试图做什么。

      当多个哈希码相同时,链表会增长,以桶数为模。然而,由于HashMap 选择素数作为其桶数,链表变得很长的最常见情况是许多哈希码实际上是相同的,而没有考虑模数。因此,简单地将存储桶的数量增加到更大的素数不会减少列表的长度:它会将列表移动到不同的存储桶,或者将其留在原来的位置,但列表的长度不会减少。

      【讨论】:

      • 值得注意的是,散列函数通常只能是“坏”的,但从数据的角度来看,它也可能是“坏”的——不管散列函数是什么,都存在一个数据设置它执行得非常糟糕(尽管根据我的经验,使用标准哈希函数时通常不必担心这一点......如果你没有对手试图让你的系统过载)。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多