哈希表双哈希

时间:2020-10-31 08:24:59

标签: algorithm data-structures hash hashmap hashtable

为我提供了两个散列函数,应将其用于插入和删除表中;

int hash1(int key)
{
    return (key % TABLESIZE);
}

int hash2(int key)
{
    return (key % PRIME) + 1;
}

我对如何利用它们感到困惑。

我愿意吗

  1. 首先使用hash1,如果该插槽是在返回值处使用的,则使用hash2?
  2. 我是否使用hash1然后将结果添加到hash2的输出中?
  3. 我是否将hash的输出用作hash2的输入?

1 个答案:

答案 0 :(得分:2)

TLDR:bucket_selected = hash1(hash2(key))


hash1()是从密钥到存储桶的身份哈希(它将密钥折叠成表的大小)。如果表大小恰好是2的幂,它将有效地屏蔽掉一些低位,并丢弃高位;例如,表大小为256时,有效返回键&255:最低有效8位。如果这些键都不存在,则将很容易发生冲突:

  • 主要是连续的数字-可能有一些小间隙,以使它们在大多数情况下清晰地映射到连续的存储桶上,或者
  • 使用的低位非常随机,因此它们分散在存储桶中

如果表大小不是2的幂,并且理想情况下是素数,则高阶位有助于将密钥分布在存储桶周围。碰撞在例如随机数,但是在计算机系统上,有时键中的不同位或多或少会发生变化(例如,存储器中的double由符号,许多尾数和许多指数位组成:如果您的数字是相似的幅度,它们之间的指数变化不大),或者存在基于二次幂边界的模式。例如,如果在uint32_t键中包含4个ASCII字符,则table_size为256的% table_size哈希仅提取字符之一作为哈希值。如果表的大小改为257,则更改任何字符都会更改所选的存储桶。

(key % PRIME) + 1几乎可以完成hash1()在表大小为素数的情况下的操作,但是为什么还要加1?某些语言的确从1为它们的数组建立索引,这是我能想到的唯一很好的理由,但是如果使用这种语言,您可能还希望hash1()也加1。为了说明hash2()的潜在用途,让我们先退后一步...

真正的通用哈希表实现需要能够创建不同大小的表(无论适合哪种程序),实际上,如果插入的元素超出了它的处理能力,则应用程序通常希望表“调整大小”或动态增长好。因此,诸如hash1之类的哈希函数将依赖于哈希表实现或调用代码来告知其当前表大小。如果哈希函数可以独立于任何给定的哈希表实现编写而仅需要键作为输入,通常会更方便。许多散列函数的作用是将密钥散列到一定大小的数字,例如uint32_tuint64_t。显然,这意味着哈希值可能比哈希表中的存储桶更多,因此使用了%操作(如果#存储桶是2的幂,则使用更快的按位操作-&操作)将哈希值“折叠”回存储桶中。因此,良好的哈希表实现通常会接受生成例如uint32_tuint64_t输出,并在内部进行%&

对于hash1-可以使用:

  • 作为身份哈希将折叠到存储桶中,或者
  • 哈希值另一个哈希函数折叠到存储桶中。

在第二种用法中,第二个哈希函数可以是hash2。如果提供给hash2的密钥通常比使用的PRIME大得多,而PRIME反过来又比存储桶数大得多,那么这可能是有道理的。为了解释为什么这是理想的,让我们再退后一步...

假设您有8个存储桶和一个哈希函数,该哈希函数以均匀的概率生成[0..10]范围内的数字:如果您将哈希值%放入表大小,则哈希值0..7将映射到存储桶0..7,哈希值8..10将映射到存储桶0..2:存储桶0..2的碰撞次数大约是其他存储桶的两倍。当散列值的范围大大大于存储桶数时,将某些存储桶%设置为比其他存储桶多一遍的重要性就很小。另外,如果您说一个散列函数输出32位数字(因此,不同的散列值的数目是2的幂),则%的较小的2的幂将映射确切地< / em>每个存储桶中具有相同数量的哈希值。

所以,让我们回到我先前的断言:hash2()的潜在实用工具实际上是这样使用的:

bucket_selected = hash1(hash2(key))

在以上公式中-hash1分布在各个存储桶中,但阻止了对存储桶的越界访问;合理地工作hash2应该输出比存储桶数大很多的数字范围,但除非键的范围大于PRIME,否则它什么都不会做,理想情况下它们的范围比PRIME大得多,从而增加了hash2(key)中散列值的几率,从而在1和PRIME之间形成了近乎均匀的分布。