在对哈希数取模后,您不会得到一个随机数吗?

问题描述

我试图理解哈希表,从我所看到的模运算符中,我们可以选择将密钥放置在哪个存储桶中。我知道哈希算法应该使不同输入的相同结果最小化,但是我不明白在模运算之后如何将不同输入的相同结果减到最小。假设我们有一个近乎完美的哈希函数,它给出了0到100,000之间的不同哈希值,然后将结果取20为模(在我们的示例中,我们有20个存储桶),结果数不是非常接近0到19之间的随机数?大致表示最终结果是0到19之间的任何数字的概率约为20的1?如果是这种情况,那么原始的哈希函数似乎无法确保最小的冲突,因为在进行模运算之后,我们最终会得到一个类似于随机数的东西?我一定是错的,但是我想确保最大程度地减少冲突的不是原始哈希函数,而是我们有多少个存储桶。

我确定我误会了。有人可以解释吗?

解决方法

我认为您对情况有正确的了解。

哈希函数和存储桶的数量都会影响发生碰撞的机会。例如,考虑一下最糟糕的散列函数-一个返回恒定值的散列函数。无论您有多少个存储桶,所有条目都将集中到同一个存储桶中,并且您有100%的碰撞机会。

另一方面,如果您具有(近乎)完美的哈希函数,则存储桶的数量将是发生碰撞机会的主要因素。如果您的哈希表只有20个存储桶,则冲突的最小机会实际上将是20分之一(随着时间的推移)。如果哈希值分布不均匀,那么至少在其中一个存储桶中发生碰撞的机会就会更高。您拥有的铲斗越多,发生碰撞的机会就越少。另一方面,存储桶过多会占用更多内存(即使它们为空),即使冲突更少,也会最终降低性能。

,

对散列数取模后,您不会得到一个随机数吗?

这取决于哈希函数。

假设您有一个用于数字的标识哈希-h(n)= n-然后,如果要哈希的键通常是递增的数字(也许偶尔会有泄漏),则在哈希之后,它们通常仍会击中连续的存储桶(包装)从最后一个存储桶到第一个存储桶的某个时间点),总体上具有较低的碰撞率。不是很随机,但是效果很好。如果密钥是随机的,则效果仍然很好-请参见下面有关随机但可重复的哈希的讨论。问题在于,当密钥既不粗略递增也不接近随机数时,身份哈希可以提供可怕的冲突率。 (您可能会认为“这是一个非常糟糕的示例哈希函数,没有人会这样做;实际上,大多数C ++标准库实现的整数哈希函数都是身份哈希)。

另一方面,如果您具有一个哈希函数,该函数表示要被哈希处理的对象的地址,并且它们都是8字节对齐的,那么如果采用mod且存储桶计数也是8的倍数,那么您只会散列到每个第8个存储桶,因此发生的碰撞次数比您预期的多8倍。不太随机,效果也不佳。但是,如果存储桶的数量是素数,那么地址将倾向于在存储桶上随机散布得多,并且效果会更好。这就是GNU C ++标准库倾向于使用存储桶的质数的原因(Visual C ++使用二乘幂的存储桶,因此它可以利用按位AND将哈希值映射到存储桶,因为AND需要一个CPU周期,而MOD可以花费30-40个周期-取决于您的确切CPU-参见here)。

当所有输入在编译时就知道了,并且输入不太多时,通常可以创建一个完美的哈希函数(为此专门设计了GNU gperf软件),这意味着它将计算出一个数字所需的存储桶数和避免任何冲突的哈希函数,但是哈希函数的运行时间可能比通用函数要长。

人们通常有一个幻想的概念-在问题中也可以看到-一个“完美的哈希函数”-或至少有很少冲突-在一些较大的数字哈希范围内,将在实际使用中提供最小的冲突哈希表,因为这个stackoverflow问题确实是要解决这个概念的错误之处。如果键映射到较大的哈希范围内的方式和概率仍然存在,那不是真的。

用于运行时输入的通用高质量哈希函数的黄金标准是,即使在进行模运算之前,也可以将其称为“随机但可重复” 。同样适用于存储桶选择(即使对存储桶选择也使用了笨拙且不那么宽容的AND位掩码方法)。

您已经注意到,这确实意味着您将在表中看到冲突。如果您可以利用键中的模式来减少这种随机但可重复的质量所能给您带来的碰撞,那么请务必充分利用。如果不是这样,则散列的好处在于,使用随机但可重复的散列,您的冲突在统计上与您的负载系数(存储的元素数除以存储桶数)有关。

例如,对于separate chaining-当您的负载系数为1.0时,1 / e(〜36.8%)的存储桶将趋于空,而另外1 / e(〜36.8%)的存储桶则为一个, 1 /(2e)或〜18.4%的两个元素,1 //(3!e)约6.1%的三个元素,1 //(4!e)或〜1.5%的四个元素,1 //(5!e)的〜.3%有五个等。-无论表中有多少个元素(即,是否有100个元素和100个桶,或1亿个元素和1亿个桶),来自非空桶的平均链长约为1.58。为什么说查找/插入/擦除是O(1)恒定时间操作。

我知道哈希算法应该使不同输入的相同结果最小化,但是我不明白在模运算之后如何将不同输入的相同结果最小化。

后模仍然是正确的。最小化相同结果意味着每个后模值具有(大约)相同数量的键映射到它。如果密钥的使用统计分布不均匀,我们特别关注存储在表中的使用中密钥。使用具有随机但可重复质量的散列函数,后模映射将出现随机变化,但总体而言,它们将足够接近以达到大多数实际目的的均衡。


回顾一下,让我直接解决这个问题:

我们只说我们有一个近乎完美的哈希函数,它给出了0到100,000之间的不同哈希值,然后我们将结果取20为模(在我们的示例中,我们有20个存储桶),结果数不是很接近0到19之间的随机数?大致表示最终结果是0到19之间的任何数字的概率约为20的1?如果是这种情况,那么原始的哈希函数似乎无法确保最小的冲突,因为在进行模运算之后,我们最终会得到一个类似于随机数的东西?我一定是错的,但是我想确保最大程度地减少冲突的不是原始哈希函数,而是我们有多少个存储桶。

所以:

  • 随机性很好:如果您获得类似随机但可重复的哈希质量,那么您的平均哈希冲突在统计上将被限制在较低的水平,并且在实践中,您不太可能看到特别可怕的碰撞链,只要您保持合理的负载系数(例如

  • 表示,您的“接近完美的哈希函数...介于0到100,000之间”可能是高质量的,也可能不是高质量的,取决于值的分布是否具有会产生冲突的模式。如果对这种模式有疑问,请使用具有随机但可重复的质量的哈希函数。

如果您使用随机数而不使用哈希函数会发生什么?然后对它做模?如果您两次调用rand(),您将获得相同的数字-我猜是一个适当的哈希函数不能执行此操作,或者可以吗?甚至散列函数也可以为不同的输入输出相同的值。

此注释显示您正在努力应对随机性的希望-希望在我的回答的较早部分中您现在已经清楚了,但是无论如何要点是随机性是好的,但它必须是可重复的:同一个键具有产生相同的模前哈希值,因此模后值会告诉您应该在其中的存储桶。

作为一个随机但可重复的示例,假设您使用rand()来填充uint32_t a[256][8]数组,然后可以对任意8个字节的密钥进行哈希处理(例如,包括一个double)通过对随机数进行XOR:

auto h(double d) {
    uint8_t i[8];
    memcpy(i,&d,8);
    return a[i[0]] ^ a[i[1]] ^ a[i[2]] ^ ... ^ a[i[7]];
}

这会产生接近理想的(rand()不是高质量的伪随机数生成器)随机但可重复的哈希,但是具有需要查询较大的内存块的哈希函数可以轻松实现会因缓存未命中而变慢。

根据[Mureinik]的描述,假设您具有完善的哈希函数,假设您的数组/存储桶已满75%,则对哈希函数进行模运算可能会导致75%的冲突概率。如果是这样,我认为它们会更好。虽然我只是在学习它们现在如何工作。

假设高质量的哈希函数正确率是75%/ 75%,则假设:

  • 封闭式散列/开放式寻址,通过查找备用存储桶来解决冲突,或者
  • 当75%的存储桶中有一个或多个链接的元素(这很可能意味着负载因子(当您谈论表的“满”程度时,很多人可能会想到”)时,单独链接已经大大增加了超过75%)

关于“我认为它们要好得多。” -实际上,这还可以,正如我在前面的回答中提到的碰撞链长度的百分比所证明的。