【问题标题】:Why is the hash table resized by doubling it?为什么哈希表通过加倍来调整大小?
【发布时间】:2015-08-03 15:43:21
【问题描述】:

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

  1. 很容易实现,或者
  2. 寻找素数效率太低(但我认为寻找 下一个素数超过n+=2 并使用测试素数 模是 O(loglogN),这很便宜)
  3. 或者这是我的误解,只有某些哈希表变体 只需要素数表大小?

更新:
教科书中使用素数的方式是某些属性起作用所必需的(例如,二次探测需要一个素数大小的表格来证明,例如,如果表格不完整,则将插入项目 X)。
作为重复发布的链接通常询问有关增加任何数字的问题,例如25% 或下一个质数,并且接受的答案表明我们加倍以保持调整大小操作“罕见”,因此我们可以保证摊销时间。
这并不能回答让表大小为素数并使用素数来调整大小甚至大于两倍的问题。所以我们的想法是在考虑调整大小开销的情况下保持素数大小的属性

【问题讨论】:

  • stackoverflow.com/a/1147232/1076640 也有很好的讨论。特别关注包含“所以你依赖散列函数而不使用偶数乘数”的部分。
  • 表格大小为 2 的幂时的查找速度更快,因为余数可以使用位掩码完成,但这更像是一种微优化。
  • 而且java的哈希表是作为外链实现的,所以没有问题。我没有关注这个问题。
  • 我们应该记住的是,内置的 Java 集合都是基于某种程度的折衷:它们必须相当好地适用于极其广泛的使用模式。在应用程序中,您可以使用更适合您的特定用例的算法重新实现集合,但代价是在其他情况下表现更差。这是很多人所做的。

标签: java performance algorithm data-structures hashtable


【解决方案1】:

问:但大多数教科书都说最适合表格的大小是素数。

Regarding size primality:

大小的素数取决于您选择的冲突解决算法。一些算法需要素数表大小(双散列、二次散列),而其他算法则不需要,它们可以从表大小的 2 次方中受益,因为它允许非常便宜的模运算。但是,当最接近的“可用表大小”相差 2 倍时,哈希表的内存使用可能不可靠。因此,即使使用线性散列或单独链接,您也可以选择非 2 大小的幂。在这种情况下,反过来,选择特定的素数大小是值得的,因为:

如果您选择素数表大小(因为算法需要这样做,或者因为您对 2 的幂大小暗示的内存使用不可靠性不满意),表槽计算(以表大小为模)可以与散列结合使用。请参阅this answer 了解更多信息。

当散列函数分布不好时(来自 Neil Coffey 的回答),2 的幂表大小是不可取的这一点是不切实际的,因为即使你有不好的散列函数,雪崩它仍然使用 2 的幂大小会比切换到素数表大小更快,因为在现代 CPU 上,单个整数除法仍然比良好的雪崩功能所需的多次乘法和移位操作要慢,例如。 G。来自 MurmurHash3。


问: 老实说,我对你是否真的推荐素数有点迷茫。似乎取决于哈希表变体和哈希函数的质量?

  1. 哈希函数的质量无关紧要,您始终可以通过 MurMur3 avalancing 来“改进”哈希函数,这比从 2 的幂表大小切换到素数表大小更便宜,见上文。

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

    否则,我建议使用带有线性探测的 2 次幂大小的哈希表。

问:加倍的方法是因为:
很容易实现,或者

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

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

此外,在我看来,在 95% 的现代哈希表案例中,这种方式过于简化,动态系统的行为并不理想。

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

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

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

问:或者这是我的误解,只有某些哈希表变体只需要素数表大小?

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

【讨论】:

  • 感谢您的回答。首先when closest "available table sizes" differ in 2 times memory usage of hash table might be unreliable是什么意思?
  • 老实说,如果你真的推荐素数,我有点迷失了。似乎这取决于散列表变体散列函数的质量?
  • @Jim 与“使用2的幂表大小,最小调整因子为2,一般我们不能保证哈希表的实际负载因子会高于0.5”的意思相同。
  • I recommend choosing prime size, with QHash or quadratic hash algorithm (aren't same), only when you need precise control over hash table load factor and predictably high actual loads 但是对于二次版本的负载表应该小于 50% 才有效
  • Otherwise, I recommend to go with power-of-2 sized hash table with linear probing. 你的意思是你也推荐外部链接?
【解决方案2】:

Java HashMap (java.util.HashMap) 在链表(或 [从 JDK8 开始] 树,具体取决于 bin 的大小和溢出)中链接桶冲突。

因此,关于二次探测功能的理论不适用。 似乎“对哈希表使用素数大小”的消息已经脱离了它多年来适用的情况......

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

我将观察到“在重新散列时重新分配碰撞链对于从 2 到 2 的幂的表来说是轻而易举的事”。

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

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

(我很确定)Java.util.HashMap 只放大(从不缩小)。但是,将哈希表减半与将哈希表加倍具有相似的效率。

这种策略的一个“缺点”是Object 的实现者没有明确要求确保哈希码的低位分布良好。 一个完全有效的散列码可以在整体上很好地分布,但在其低位中分布很差。因此,当实际用于HashMap 时,遵守hashCode() 的一般合同的对象可能仍会失败! Java.util.HashMap 通过在提供的 hashCode() 实现上应用额外的哈希“扩展”来缓解这种情况。这种“传播”非常粗略(将 16 个高位与低位异或)。

对象实现者应该意识到(如果还没有意识到)哈希码中的偏差(或缺乏偏差)会对使用哈希的数据结构的性能产生重大影响。

作为记录,我基于此来源副本进行了分析:

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

【讨论】:

  • 非常有趣的分析。但是JDK确实通过加倍来调整表的大小。我假设他们选择了外部 chaning,因为它很容易实现。但是我想知道是否不能将素数表与您提到的相同“技巧”一起使用
  • 还有关于将你提到的哈希表减半。我从来没有读过任何教科书,甚至暗示过这种技术。这实际上用于任何实现吗?
  • @Jim 是的,它确实通过加倍调整大小。 “诀窍”是通过意识到碰撞桶被整齐地减半来简化加倍。在 JDK8 之前,代码显然没有使用 that 技巧。减半是一个经常被忽视的操作。然而,对于复杂或长时间运行的应用程序,它可能是必要的。我注意到Java.util.HashMap 没有打扰。有没有注意到服务器如何从定期重启中受益?诸如不卸载类和内部“缓冲区”大小增加但从未减小等阻塞是永不泄漏的系统不必要地占用空闲资源的原因。
  • 我不确定这是否能解决您的问题。即使在 C++ 中,当您释放内存时,它也不会进入系统进行重用,而是保留以供将来请求中的同一进程重用。结果,整个过程的内存永远不会下降。对于您长时间打开的 Firefox 或 chrome 等应用程序,这一点非常明显。他们最终会消耗您的大部分系统。因此,我不确定将桌子减半的努力是否会使我们免于您正确指出的重新启动期间。我说得通吗?
  • @Jim。 C++ 标准没有这样的规定。这是讨论newdelete 背后发生的事情的话题。以 Visual Studio 为例,他们呼叫malloc(.)free()。您将在此处注意到free(.) 可能会导致资源被退回。 msdn.microsoft.com/en-us/library/we1whae7.aspx。这将是一个非常糟糕的平台/运行时,从未 交还资源。请参阅 Windows 95/98 来证明这一点!他们没有这样做,但同样被认为是“最糟糕的操作系统。永远。”
猜你喜欢
  • 2011-01-23
  • 1970-01-01
  • 2011-06-24
  • 2013-12-21
  • 2014-02-23
  • 1970-01-01
  • 2012-10-14
  • 2021-03-14
相关资源
最近更新 更多