为什么哈希表扩展通常通过加倍大小来完成?

时间:2010-03-03 07:37:13

标签: algorithm data-structures hash hashtable

我已经对哈希表进行了一些研究,并且我一直在运行经验,当有一定数量的条目(最大或通过75%的加载因子)时,应该扩展哈希表

几乎总是,建议将哈希表的大小加倍(或加倍加1,即2n + 1)。但是,我找不到合理的理由。

为什么要加倍大小,而不是将其增加25%,或者将其增加到下一个素数或下一个素数(例如三个)?

我已经知道,选择一个初始哈希表大小是一个素数通常是一个好主意,至少如果你的哈希函数使用模数,如通用哈希。而且我知道这就是为什么通常建议做2n + 1而不是2n(例如http://www.concentric.net/~Ttwang/tech/hashsize.htm

然而正如我所说,我没有看到任何真正的解释,为什么加倍或加倍加一实际上是一个不错的选择,而不是选择新哈希表的大小的其他方法。

(是的,我读过关于哈希表的维基百科文章:) http://en.wikipedia.org/wiki/Hash_table

6 个答案:

答案 0 :(得分:36)

例如,如果调整大小是一个恒定的增量,则哈希表不能声明“分摊的常量时间插入”。在这种情况下,调整大小的成本(随着散列表的大小而增长)将使一个插入的成本在要插入的元素总数中呈线性。因为随着表的大小调整大小变得越来越昂贵,所以必须“越来越少”地发生以保持摊销的摊销成本不变。

大多数实现允许平均存储桶占用增长到调整之前预先固定的边界(0.5到3之间的任何值,这些都是可接受的值)。通过这种约定,在调整大小之后,平均桶占用率变为一半。通过加倍调整大小可以使平均桶占用宽度为* 2。

子注意:由于统计聚类,如果您希望许多存储桶最多只有一个元素(最大速度可以忽略缓存大小的复杂影响),则必须将平均存储桶占用率调低至0.5,或者如果你想要最少数量的空桶(对应于浪费的空间),则高达3。

答案 1 :(得分:8)

我在这个网站上已经阅读了关于增长战略的一个非常有趣的讨论......只是找不到它。

虽然常用2,但已证明它不是最佳值。一个经常被引用的问题是它不能很好地处理分配器方案(通常分配两个块的功率),因为它总是需要重新分配,而较小的数字实际上可能在同一个块中重新分配(模拟就地增长)因此速度更快。

因此,例如,VC++标准库在广泛讨论之后使用增长因子1.5(理想情况下,如果使用首先适合的内存分配策略,则应该是黄金数)。邮件列表。解释的原因是here

  
    
      

如果任何其他矢量实现使用2以外的增长因子,我会感兴趣,而且我也想知道VC7是使用1.5还是2(因为我这里没有那个编译器)。

    
         

技术上有理由倾向于使用1.5到2 - 更具体地说,更喜欢小于1+sqrt(5)/2的值。

         

假设您正在使用第一个适合的内存分配器,并且您逐渐附加到向量。然后每次重新分配时,分配新内存,复制元素,然后释放旧内存。这留下了一个空白,最终能够使用那个内存会很好。如果向量增长太快,它对于可用内存总是太大。

         

事实证明,如果增长因子是>= 1+sqrt(5)/2,那么新的记忆总是对于已经留下的洞来说太大了;如果是< 1+sqrt(5)/2,则新内存最终会适合。因此,1.5足够小,可以回收内存。

  
     

当然,如果增长因子是>= 2,那么新记忆对于迄今为止留下的洞来说总是太大了;如果是< 2,则新内存最终会适合。推测(1+sqrt(5))/2的原因是......

     
      
  • 初始分配为s
  •   
  • 第一次调整大小为k*s
  •   
  • 第二个调整大小为k*k*s,适合{if k*k*s <= k*s+s的洞,即iff k <= (1+sqrt(5))/2
  •   
     

......这个洞可以尽快回收。

     

通过存储其先前的大小,可以生长纤维蛋白。

当然,它应该根据内存分配策略进行调整。

答案 2 :(得分:4)

特定于散列容器的大小加倍的一个原因是,如果容器容量始终是2的幂,那么不是使用通用模来将散列转换为偏移,而是可以实现相同的结果位移。 Modulo是一个缓慢的操作,原因与整数除法相同。 (整数除法是否为&#34;慢&#34;在程序中的任何其他内容的上下文中当然是依赖于案例的,但它肯定比其他基本整数算法慢。)

答案 3 :(得分:3)

扩展任何类型的集合时加倍内存是一种常用的策略,可以防止内存碎片而不必经常重新分配。正如您所指出的,可能有理由拥有大量元素。在了解您的应用程序和数据时,您也可以预测元素数量的增长,从而选择另一个(更大或更小)增长因子而不是倍增。

库中的常规实现正是:一般实现。他们必须专注于在各种不同情况下做出合理选择。在了解上下文时,几乎总是可以编写更专业,更有效的实现。

答案 4 :(得分:3)

如果你不知道有多少对象你将最终使用(假设为N), 通过加倍空间,你最多可以记录 2 N个重新分配。

我认为如果您选择正确的首字母“n”,则增加赔率 2 * n + 1将在随后的重新分配中产生素数。

答案 5 :(得分:3)

同样的理由适用于将矢量/ ArrayList实现的大小加倍,请参阅this answer