为什么哈希表通过加倍来调整大小?

时间:2015-05-21 19:33:25

标签: java performance algorithm data-structures hashtable

检查java并在线搜索哈希表代码示例,似乎通过加倍来完成表的大小调整。
但大多数教科书都说桌子的最佳尺寸是素数 所以我的问题是:
加倍的方法是因为:

  1. 易于实施,或
  2. 找到素数太低效了(但我觉得这个发现 下一个素数超过n+=2并使用测试素数 modulo是O(loglogN),便宜)
  3. 或者这是我的误解,只有某些哈希表变体 只需要素数表?
  4. 更新
    某些属性需要使用素数在教科书中呈现的方式(例如,二次探测需要一个主要大小的表来证明,例如,如果一个表不是完整的项目X将被插入)。
    发布为重复的链接通常要求增加任何数量,例如25%或下一个素数和接受的答案表明我们加倍,以保持调整大小操作“罕见”,以便我们可以保证摊销时间。
    这并没有回答这样一个问题:使用一个表格大小是素数并使用素数来调整大小甚至大于两倍。因此,我们的想法是保持主要大小的属性考虑到调整大小开销

2 个答案:

答案 0 :(得分:6)

  

问:但是大多数教科书都说桌子的最佳尺寸是素数。

Regarding size primality:

  

什么是大小的素数,它取决于您选择的碰撞解决算法。有些算法需要素数表大小(双重散列,二次散列),其他算法不需要,并且它们可以从2的幂的表大小中受益,因为它允许非常便宜的模运算。但是,当最近的"可用的表格大小"两次不同,哈希表的内存使用可能不可靠。因此,即使使用线性散列或单独链接,您也可以选择2大小的非幂。在这种情况下,反过来,值得选择特定的素数,因为:

  如果您选择主表大小(因为算法需要这个,或者因为您对2的幂大小暗示的内存使用不可靠性不满意),表槽计算(按表大小模数)可以与散列相结合。有关详情,请参阅this answer

     

当散列函数分布不好(来自Neil Coffey的答案)时,表2的幂大小是不可取的这一点是不切实际的,因为即使你有糟糕的散列函数,也会对它进行雪崩并仍然使用2的幂切换到素数表大小的大小会更快,因为在现代CPU上单个积分除法仍然较慢,因为良好的雪崩函数需要多个多重复用和移位操作,例如: G。来自MurmurHash3。

  

问:老实说,如果你真的推荐素数,我会迷失一点。似乎它取决于哈希表变量和哈希函数的质量?

  1. 哈希函数的质量并不重要,你总是可以改善"通过MurMur3平衡的哈希函数,比从2表格大小切换到素数表大小便宜,见上文。

  2. 我建议您选择素数大小,使用QHash或二次散列算法(aren't same),当您需要精确控制哈希表加载因子可预测的高实际负载。对于2的幂表大小,最小调整大小因子是2,并且通常我们不能保证哈希表将具有任何高于0.5的实际负载因子。 See this answer.

    否则,我建议使用线性探测功能的2-power大小哈希表。

  3.   

    问:接近加倍是因为:
      它易于实现,或

    基本上,在许多情况下,是的。 见this large answer regarding load factors

      

    负载因子不是哈希表数据结构的重要组成部分 - 它是为动态系统定义行为规则的方法(增长/缩小哈希表是一个动态系统)。

      此外,在我看来,95%的现代哈希表情况都是这种方式过度简化,动态系统表现不佳。

    什么是加倍?它只是最简单的调整大小策略。该策略可能是任意复杂的,在您的用例中表现最佳。它可以考虑当前的哈希表大小,增长强度(自上次调整大小以来完成了多少操作)等等。没有人禁止你实现这样的自定义大小调整逻辑。

      

    问:发现素数效率太低(但我认为找到下一个素数超过n + = 2并使用模数测试素数是O(loglogN)这很便宜)

    有一个很好的做法是预先计算一些主要哈希表大小的子集,在运行时使用二进制搜索在它们之间进行选择。请参阅the list double hash capacities and explainationQHash capacities。或者,即使使用direct lookup,这也非常快。

      

    问:或者这是我的误解,只有某些散列表变体只需要主表大小?

    是的,只有某些类型需要,见上文。

答案 1 :(得分:3)

Java HashMap(java.util.HashMap)链接链表中的桶冲突(或[从JDK8开始]树,具体取决于容器的大小和过量填充)。

因此,关于二次探测功能的理论并不适用。 似乎消息'使用散列表的素数大小'已经脱离了多年来适用的情况......

使用2的幂具有将哈希值减少到表条目的优点(如其他答案所述)可以通过位掩码来实现。整数除法相对昂贵,在高性能情况下,这可能有所帮助。

我会观察到这一点"重新分配碰撞链时,重新划分对于2的幂为2的幂的表来说很简单。

请注意,当使用两次重复的力量到两倍大小时,分裂'两个桶之间的每个桶基于' next'一点哈希码。 也就是说,如果散列表有256个桶,那么使用散列值重新散列的最低8位基于第9位拆分每个冲突链,并且要么保留在同一个桶B中(第9位为0),要么转到桶B + 256(第9位为1)。这种分裂可以保留/利用铲斗处理方法。例如,java.util.HashMap保持以相反的顺序排序的小桶,然后将它们分成遵循该顺序的两个子结构。它将大桶保存在按哈希码排序的二叉树中,并且类似地拆分树以保留该顺序。

NB:这些技巧直到JDK8才实现。

(我很确定)Java.util.HashMap只有大小(从不下降)。但是将哈希表减半使其加倍也有类似的效率。

一个缺点'该策略的目的是Object实现者​​没有明确要求确保散列码的低阶位分布良好。 完全有效的哈希码可以在整体上很好地分布,但在其低位比特中分布很差。因此,当hashCode()实际使用HashMap时,服从Java.util.HashMap的一般合同的对象可能仍然无效! hashCode()通过应用额外的哈希'传播来缓解这种情况。到提供的{{1}}实施。这种传播'是非常快的原油(16位高位和低位)。

对象implmenters应该知道(如果还没有)哈希代码中的偏差(或缺少哈希代码)会对使用哈希的数据结构的性能产生重大影响。

对于记录,我已根据此来源副本进行此分析:

http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/java/util/HashMap.java