散列表索引如何工作?

时间:2011-11-21 17:58:41

标签: algorithm data-structures hashtable

我知道创建哈希码,冲突,.GetHashCode和.Equals等之间的关系。

我不太明白是如何使用32位哈希值来进行~O(1)查找。如果你有一个足够大的数组来分配32位数字中的所有可能性,那么你确实得到了~O(1)但这会浪费内存。

我的猜测是,Hashtable类在内部创建一个小数组(例如1K项),然后将32位数重新转换为3位数,并将其用作查找。当元素数量达到某个阈值(比如说75%)时,它会将数组扩展为10K项目,并将内部哈希值重新计算为4位数,当然基于32位哈希值。

不过,在这里,我正在使用~O(1)来解释可能的碰撞及其解决方案。

我是否有正确的要点,或者我是否完全脱离了标记?

3 个答案:

答案 0 :(得分:7)

  

我的猜测是,Hashtable类在内部创建一个小数组(例如1K项),然后将32位数重新转换为3位数,并将其用作查找。

这正是发生的情况,除了表的容量( bins 的数量)通常设置为2的幂或素数。然后以该数字为模取取哈希码,以找到要插入项目的bin。当容量为2的幂时,模数运算变为简单的位掩码运算。

  

当元素数量达到某个阈值(比如说75%)时

如果您指的是Java Hashtable实现,那么是。这称为负载系数。其他实现可以使用2/3而不是3/4。

  

它会将数组扩展为类似10K的项目

在大多数实现中,容量不会增加十倍,而是增加一倍(对于两倍大小的哈希表)或者乘以大约1.5 +到下一个素数的距离。

答案 1 :(得分:2)

哈希表有许多包含项目的bin。从一开始,垃圾箱的数量非常少。给定一个哈希码,它只是使用hashcode modulo bincount来查找项应该驻留的bin。这给出了快速查找(找到项目的bin:取得hashcode的模数,完成)。

或(伪)代码:

int hash = obj.GetHashCode();
int binIndex = hash % binCount;
// The item is in bin #binIndex. Go get the items there and find the one that matches.

显然,正如你自己想出的那样,在某些时候桌子需要增长。执行此操作时,将创建一个新的bin数组,并将表中的项重新分配到新的bin。这也意味着增长哈希表可能很慢。 (因此,在大多数情况下大约为O(1),除非插入触发内部调整大小。查找应始终为~O(1))。

答案 2 :(得分:1)

通常,哈希表处理溢出的方式有很多变化。

当负载系数(使用中的容器百分比)超过某个特定百分比时,许多(包括Java,如果内存服务)调整大小。这样做的缺点是速度是不可靠的 - 大多数插入将是O(1),但是一些将是O(N)。

为了改善这个问题,有些人会逐渐调整大小:当负载系数超过幻数时,他们会:

  1. 创建第二个(更大的)哈希表。
  2. 将新项目插入新哈希表。
  3. 将现有哈希表中的一些项目移动到新哈希表。
  4. 然后,每个后续插入将另一个块从旧哈希表移动到新哈希表。这保留了O(1)平均复杂度,并且可以编写,因此每次插入的复杂性基本上是恒定的:当哈希表变为“满”(即,加载因子超过触发点)时,表的大小加倍。然后,每次插入时插入新项目并将一个项目从旧表格移动到新表格。旧表将在新表填满时完全清空,因此每个插入将涉及两个操作:插入一个新项目并移动一个旧项目,因此插入速度基本保持不变。

    还有其他策略。我特别喜欢的是使哈希表成为平衡树的表。有了这个,你通常完全忽略溢出。随着哈希表填满,您最终会在每棵树中添加更多项目。理论上,这意味着复杂度为O(log N),但对于任何实际大小,它与log N/M成比例,其中M =桶的数量。对于实际大小范围(例如,高达数十亿个项目),它们基本上是恒定的(log N慢慢地非常)并且对于可以放入内存的最大表来说通常会快一点,对于较小的尺寸,损失更快。缺点是,当您存储的对象相当大时,它才真正实用 - 如果您为每个节点存储(例如)一个字符,则每个节点的两个指针(通常是平衡信息)的开销将非常大高。