Why should hash functions use a prime number modulus?
很久以前,我花了1.25美元从交易桌上买了一本数据结构书。在本文中,对散列函数的解释说,由于"数学的本质",它最终应该被一个素数修改。
你对一本1.25美元的书有什么期望?
不管怎样,我已经有好几年的时间来思考数学的本质,但还是搞不清楚。
即使存在质数的桶,数字的分布是否真的更大?或者这是一个老的程序员的故事,每个人都接受,因为其他人都接受它?
通常,一个简单的哈希函数的工作原理是获取输入的"组件部分"(字符串中的字符),然后将它们乘以某个常量的幂,然后将它们以某种整数类型添加到一起。例如,字符串的典型散列(尽管不是特别好)可能是:好的。
1 | (first char) + k * (second char) + k^2 * (third char) + ... |
然后,如果输入的字符串都具有相同的第一个字符,那么结果将都是相同的模k,至少直到整数类型溢出为止。好的。
作为一个例子,Java的字符串哈希代码与此类似,它用K=31来做字符倒序。因此,在以相同方式结束的字符串之间得到了显著的关系模31,在除接近结尾之外相同的字符串之间得到显著的关系模2^32。这不会严重破坏哈希表行为。]好的。
哈希表的工作原理是将哈希的模取到存储桶的数量上。好的。
在哈希表中,不要在可能的情况下产生冲突是很重要的,因为冲突会降低哈希表的效率。好的。
现在,假设有人将一整串值放入一个哈希表中,该哈希表在项目之间具有某种关系,就像所有项目都具有相同的第一个字符一样。我想说,这是一个相当可预测的使用模式,所以我们不希望它产生太多的冲突。好的。
事实证明,"由于数学的本质",如果散列中使用的常量和桶的数量是互质的,那么在一些常见的情况下,碰撞会最小化。如果它们不是互质的,那么在输入之间就有一些相当简单的关系,对于这些关系,碰撞不会最小化。所有散列的模都等于公因子,这意味着它们都将落在桶的第1/n个数中,这些桶的值模等于公因子。你会得到n倍的碰撞,其中n是公因数。因为n至少是2,所以对于一个相当简单的用例来说,产生的碰撞至少是正常情况的两倍是不可接受的。如果某个用户要将我们的分发分成桶,我们希望它是一个反常的意外,而不是一些简单的可预测的用法。好的。
现在,哈希表实现显然无法控制放入其中的项。他们不能阻止他们的关系。所以要做的就是确保常量和桶数是互质的。这样,就不需要仅仅依靠"最后一个"分量就可以根据一些小的公因数来确定桶的模数。据我所知,他们不必是主要的实现这一点,只是互质。好的。
但是如果散列函数和散列表是独立编写的,那么散列表就不知道散列函数是如何工作的。它可能使用一个带有小因子的常数。如果你幸运的话,它的工作方式可能完全不同,而且是非线性的。如果散列值足够好,那么任何桶数都可以。但是偏执哈希表不能假定一个好的哈希函数,所以应该使用质数的bucket。类似地,一个偏执的散列函数应该使用一个较大的素数常量,以减少有人使用许多桶的可能性,这些桶恰好与该常量有一个公共因子。好的。
在实践中,我认为使用2的幂作为桶的数目是相当正常的。这很方便,而且省去了搜索或预先选择合适数量级的素数的麻烦。所以您依赖散列函数不使用偶数乘法器,这通常是一个安全的假设。但是,根据上面的散列函数,您仍然可以偶尔得到不好的散列行为,而主存储桶计数可以进一步帮助您。好的。
就我所知,提出"一切都必须是素数"的原则是在哈希表上良好分布的一个充分但不是必要的条件。它允许每个人进行互操作,而无需假定其他人遵循相同的规则。好的。
[编辑:还有另一个更专门的原因,就是使用质数的桶,如果你处理与线性探测的碰撞。然后从散列代码计算步幅,如果步幅是存储桶计数的一个因素,那么在回到开始的位置之前,只能进行(存储桶计数/步幅)探测。当然,您最想避免的情况是stride=0,它必须是特殊的cased,但是为了避免特殊的casing bucket_count/stride等于一个小整数,您可以将bucket_count设为prime,而不关心提供了什么stride,它不是0。]好的。好啊.
从哈希表插入/检索时,首先要计算给定键的哈希代码,然后通过执行hashcode%table_length将哈希代码剪裁为哈希表的大小,找到正确的存储桶。这里有两个"陈述",你可能在某个地方读过
这是证据。
如果您的hashcode函数在其他变量中产生以下hashcodes x、2x、3x、4x、5x、6x…,那么所有这些都将聚集在m个存储桶中,其中m=表的长度/greatestcommonfactor(表的长度,x)。(验证/推导这一点很简单)。现在您可以执行以下操作之一以避免集群
确保您不会生成太多的哈希代码,这些哈希代码是其他哈希代码的倍数,如in x、2x、3x、4x、5x、6x…。但是,如果您的哈希表应该有数百万个条目,这可能有点困难。或者简单地通过使greatestcommonfactor(table_length,x)等于1使m等于table_length,即使table_length与x互质。如果x可以是任意数字,则确保table_length是质数。
发件人-http://srinv.blogspot.com/2006/07/hash-table-lengthes-and-prime-numbers.html
http://computinglife.wordpress.com/2008/11/20/为什么做哈希函数使用的素数数/
漂亮的图片和清晰的解释,太。
编辑:作为一个总结,使用素数,因为你有最好的机会获得一个独特的价值,当multiplying素数数值的选择和添加他们的一切。例如,给定一个字符串,multiplying与素数的数值,然后添加你想要的一切都给它的哈希值。
一个更好的问题将是完全数31,为什么?
DR
使用素数是因为您很有可能为使用多项式模p的典型哈希函数获得唯一值。比如,对于长度<=n的字符串使用这样的哈希函数,就会发生冲突。这意味着两个不同的多项式产生相同的模P值。这些多项式的差也是一个相同次数n(或更小)的多项式。它的根数不超过n个(这里是数学的本质,因为这个声明只适用于域上的多项式=>素数)。所以,如果n远小于p,你可能不会发生碰撞。在此之后,实验可能会证明37足够大,可以避免长度为5-10的字符串哈希表发生冲突,并且足够小,可以用于计算。
提供额外的观点只是有这个网站:
http://www.codexon.com /文章/散列函数的模素数的神话
这contends,你应该使用桶数为最大可能的反对下一个素数对圆形桶数。它似乎是一合理的可能性。当以上的行,我可以了解大数A桶将是更好的,但我无法让一本数学论证。
它依赖于哈希函数的选择。
哈希函数是将各种元素的位置数据是由一些因素模量multiplying他们相应的电力两个机器字大小(这是免费的只是让模量的计算溢出)。
你不想任何公共因子之间的数据元素和一个乘数大小的哈希表,然后它可以发生,因为不同的数据元素不传播在整个数据表。如果你选择A素表的大小是不可能的,在这样一个公共因子。
在其他的手,这些因素通常是从制造的奇素数,所以你应该在你的安全使用两个哈希表的权力(例如Eclipse的Java使用31当它哪里hashcode()方法)。
Primes are unique numbers. They are
unique in that, the product of a prime
with any other number has the best
chance of being unique (not as unique
as the prime itself of-course) due to
the fact that a prime is used to
compose it. This property is used in
hashing functions.Given a string"Samuel", you can
generate a unique hash by multiply
each of the constituent digits or
letters with a prime number and adding
them up. This is why primes are used.However using primes is an old
technique. The key here to understand
that as long as you can generate a
sufficiently unique key you can move
to other hashing techniques too. Go
here for more on this topic about
http://www.azillionmonkeys.com/qed/hash.html
http://computinglife.wordpress.com/2008/11/20/为什么做哈希函数使用的素数数/
Copying from my other answer https://stackoverflow.com/a/43126969/917428. See it for more details and examples.
号
我认为,这只与计算机在BASE2中工作的事实有关。想一想同样的东西是如何在基础10上工作的:
- 8%10=8
- 18%10=8
- 87865378%10=8
数字是什么并不重要:只要以8结尾,它的模10就是8。
选择一个足够大的、非二次幂的值,将确保哈希函数实际上是所有输入位的函数,而不是它们的一个子集。
假设您的表大小(或模数)是t=(b*c)。现在,如果输入的散列类似于(n*a*b),其中n可以是任何整数,那么输出将不会得到很好的分布。因为每次n变为c、2c、3c等时,输出都会开始重复。也就是说,您的输出将只分布在C位置。注意这里的c是(t/hcf(表大小,哈希))。
这个问题可以通过制造HCF 1来解决。素数是非常好的。
另一个有趣的事情是,当t为2^n时,输出将与输入散列的所有低n位完全相同。由于每个数都可以表示2的幂,当我们用t取任意数的模时,我们将减去2形式数的所有幂,即大于等于n,因此总是根据输入给出特定模式的数目。这也是一个糟糕的选择。
同样地,t as 10^n也是不好的,因为类似的原因(以数字的十进制记数法代替二进制记数法)。
因此,素数倾向于给出更好的分布结果,因此是表大小的好选择。
我想为史蒂夫·杰索普的回答做些补充(因为我没有足够的声誉,所以我不能对此发表评论)。但我找到了一些有用的材料。他的回答很有帮助,但他犯了一个错误:桶的大小不应该是2的幂。我将引用托马斯·科尔曼、查尔斯·雷瑟森等人在第263页的《算法导论》一书:
When using the division method, we usually avoid certain values of m. For example, m should not be a power of 2, since if m = 2^p, then h(k) is just the p lowest-order bits of k. Unless we know that all low-order p-bit patterns are equally likely, we are better off designing the hash function to depend on all the bits of the key. As Exercise 11.3-3 asks you to show, choosing m = 2^p-1 when k is a character string interpreted in radix 2^p may be a poor choice, because permuting the characters of k does not change its hash value.
号
希望有帮助。
我读过一个热门的WordPress网站,上面有一些热门的答案。根据我的理解,我想分享一下我做的一个简单的观察。
您可以在本文中找到所有详细信息,但假定以下内容正确:
- 使用素数给我们一个唯一值的"最佳机会"
一个普通的hashmap实现需要两件事是唯一的。
- 键的唯一哈希代码
- 存储实际值的唯一索引
如何获得唯一索引?使内部容器的初始大小也成为素数。因此,基本上,prime之所以参与进来,是因为它具有产生唯一数字的独特特性,我们最终使用这些数字来标识对象,并在内部容器中查找索引。
例子:
key="key"
value="值"
uniqueId ="k" * 31 ^ 2 +
"e" * 31 ^ 1` +
"y"
映射到唯一ID
现在我们想要一个独特的位置来体现我们的价值——所以我们
假设
我知道这是简化的,但我希望能把一般的想法讲清楚。
一个哈希函数,它的重要,不仅减少,但不可能让它colisions通常呆在相同的哈希值而变化的几个字节。
说你有一个方程:一个
F在A密钥不是素数的例子:x=1,z = 2 = 8和密钥因为密钥/ Z = 4仍然是一个自然数,4变的解决方案的方程和我们在这个案例(N / 2)×Y键是N的N为每一个低的解决方案的方程有实用的8倍,因为A不是素数。
如果我们已经知道attacker 8是可能的解决方案的方程可以从变更文件产生的氦4和8个安静变得相同的哈希。