很久以前,我花了1.25美元从讲价表上买了一本数据结构书。在其中,对散列函数的解释说,由于“数学的性质”,它最终应按质数进行模运算。
您对1.25美元的书有什么期待?
无论如何,我已经花了很多年考虑数学的本质,但仍然无法弄清楚。
即使存储桶数是素数,数字的分布确实真的更多吗?还是这是每个人都因为别人都接受而接受的古老程序员的故事?
很久以前,我花了1.25美元从讲价表上买了一本数据结构书。在其中,对散列函数的解释说,由于“数学的性质”,它最终应按质数进行模运算。
您对1.25美元的书有什么期待?
无论如何,我已经花了很多年考虑数学的本质,但仍然无法弄清楚。
即使存储桶数是素数,数字的分布确实真的更多吗?还是这是每个人都因为别人都接受而接受的古老程序员的故事?
Answers:
通常,一个简单的哈希函数通过获取输入的“组成部分”(在字符串的情况下为字符),然后将它们乘以某个常数的幂,然后将它们加在一起以某种整数类型来工作。因此,例如,字符串的典型(尽管不是特别好)哈希可能是:
(first char) + k * (second char) + k^2 * (third char) + ...
然后,如果输入一串都具有相同第一个字符的字符串,那么结果将全部取相同的模k,至少直到整数类型溢出为止。
[例如,Java的字符串hashCode与此极为相似-它以相反的顺序执行字符,k = 31。因此,在以相同方式结尾的字符串之间会得到31的模数关系,而在末端附近的相同字符串之间将有2 ^ 32的模数关系。这不会严重破坏哈希表的行为。]
哈希表的工作原理是将哈希的模数乘以存储桶数。
在哈希表中,重要的是不要在可能的情况下产生冲突,因为冲突会降低哈希表的效率。
现在,假设有人将一堆值放入一个散列表中,这些散列表中的项目之间具有某种关系,例如所有的项目都具有相同的第一个字符。我会说这是一个相当可预测的使用模式,因此我们不希望它产生太多冲突。
事实证明,“由于数学的性质”,如果哈希中使用的常量和存储区的数量是互质的,那么在某些常见情况下,冲突会最小化。如果它们不是互质的,那么在输入之间存在一些相当简单的关系,对于这些关系,不会最小化冲突。所有散列都以公因数取模相等,这意味着它们将全部落入具有该值以公因数取模的存储桶的1 / n。您得到的碰撞次数是n倍,其中n是公因子。由于n至少为2,我想说一个相当简单的用例产生的碰撞至少是正常情况的两倍是不可接受的。如果某些用户打算将我们的发行分为几类,我们希望这是一次怪胎事故,而不是一些简单的可预测用法。
现在,哈希表实现显然无法控制放入其中的项目。他们无法阻止他们之间的联系。因此,要做的是确保常数和存储桶数互质。这样一来,您就不会仅依靠“最后一个”组件来确定铲斗相对于一些小的公因子的模数。据我所知,他们并不一定要做到这一点,而只需同质。
但是,如果哈希函数和哈希表是独立编写的,则哈希表将不知道哈希函数的工作方式。它可能使用的常数很小。如果幸运的话,它的工作原理可能会完全不同,而且可能是非线性的。如果哈希足够好,那么任何存储桶计数都很好。但是偏执的哈希表不能假设一个好的哈希函数,因此应使用素数的存储桶。类似地,偏执散列函数应使用较大的质数常数,以减少某人使用多个存储桶的机会,这些存储桶恰好与该常数具有公因子。
实际上,我认为使用2的幂作为存储桶数是很正常的。这很方便,省去了搜索或预先选择合适大小的质数的麻烦。因此,您依赖哈希函数不使用偶数乘法器,这通常是一个安全的假设。但是您仍然会偶尔基于上述散列函数获得不良的散列行为,并且主存储桶计数可以进一步提供帮助。
据我所知,提出“一切都必须是素数”的原则,是在哈希表上进行良好分布的充分但非必要条件。它允许每个人进行互操作,而无需假设其他人遵循相同的规则。
[编辑:还有另一个更专业的原因使用质数的存储桶,即如果您使用线性探测来处理碰撞。然后,您可以根据哈希码计算跨度,如果跨度成为存储桶计数的一个因素,那么您只能在返回起点之前进行(bucket_count /跨步)探测。当然,您最想避免的情况是stride = 0,这必须是特殊情况,但要避免还使用特殊情况的bucket_count / stride等于一个小整数,您可以使bucket_count为素数,而不必关心跨度不为0。]
从哈希表插入/检索哈希表时,您要做的第一件事是计算给定键的hashCode,然后通过执行hashCode%table_length将hashCode修剪为hashTable的大小来找到正确的存储桶。这是您最有可能在某处阅读的2份“声明”
这是证明。
如果假设您的hashCode函数在以下{x,2x,3x,4x,5x,6x ...}中导致以下hashCode,那么所有这些将被聚类到m个存储桶中,其中m = table_length / GreatestCommonFactor (table_length,x)。(验证/得出这一点很简单)。现在,您可以执行以下操作之一以避免聚类
确保您不会生成太多的哈希码,这些哈希码是另一个哈希码的倍数,例如{x,2x,3x,4x,5x,6x ...}。但是,如果您的hashTable应该具有数百万个条目。或者通过使GreatestCommonFactor(table_length,x)等于1,即通过使table_length与x互质来使m等于table_length。如果x可以是任意数,请确保table_length是质数。
来自-http: //srinvis.blogspot.com/2006/07/hash-table-lengths-and-prime-numbers.html
http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
解释很清楚,也有图片。
编辑:作为总结,使用质数,因为当将值乘以所选的质数并将它们加起来时,您就有最大的机会获得唯一值。例如,给定一个字符串,将每个字母值与质数相乘,然后将所有字母值相加,即可得到其哈希值。
更好的问题是,为什么数字精确为31?
*32
是一个简单的位移,甚至更好的是立即地址比例因子(例如lea eax,eax*8; leax, eax,eax*4
在x86 / x64上)。所以*31
是一个很好的候选人素数相乘。几年前,这几乎是真的-现在最新的CPU架构几乎具有即时乘法功能-除法总是很慢...
index[hash(input)%2]
将导致所有可能的哈希值和值范围的一半发生冲突。 index[hash(input)%prime]
导致所有小于2的哈希冲突。将除数固定为表格大小还可以确保该数字不能大于表格。
之所以使用质数,是因为您有很好的机会为典型的使用多项式取模P的哈希函数获得唯一值。假设,对于长度<= N的字符串使用此类哈希函数,则会发生冲突。这意味着2个不同的多项式产生相同的模P值。这些多项式的差再次是相同次数N(或更小)的多项式。它的根数不超过N(这是数学的本质,这表明它是正确的,因为这种主张仅适用于域=>质数上的多项式)。因此,如果N远小于P,则可能不会发生碰撞。之后,实验可能会显示37足够大,可以避免长度为5-10的字符串的哈希表发生冲突,并且足够小,可以用于计算。
只是为了提供一个替代的观点,这里有这个网站:
http://www.codexon.com/posts/hash-functions-the-modulo-prime-myth
这表明您应该使用尽可能多的存储桶,而不是四舍五入为最基本的存储桶。这似乎是合理的可能性。凭直觉,我当然可以看到更多的存储桶会更好,但是我无法对此进行数学上的论证。
质数是唯一数字。它们的独特之处在于,由于使用了素数来构成素数,因此素数与任何其他数字的乘积具有最大的唯一性机会(不像素数本身那样唯一)。此属性在哈希函数中使用。
给定字符串“ Samuel”,您可以通过将每个组成数字或字母乘以质数并将它们相加来生成唯一的哈希。这就是为什么要使用素数的原因。
但是,使用素数是一种古老的技术。此处的密钥是要理解的,只要您可以生成足够唯一的密钥,就可以使用其他哈希技术。转到此处,以获取有关http://www.azillionmonkeys.com/qed/hash.html的更多信息 。
http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/
假设您的表大小(或模数)为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为10 ^ N也是不好的。
因此,质数趋向于提供更好的分布式结果,因此是表大小的不错选择。
从我的其他答案复制https://stackoverflow.com/a/43126969/917428。有关更多详细信息和示例,请参见它。
我认为这仅与计算机在base 2中可以使用的事实有关。请想一想同样的事情在base 10中如何工作:
多少是无关紧要的:只要以8结尾,其模10将为8。
选择足够大的非二乘幂数将确保哈希函数确实是所有输入位的函数,而不是它们的子集。
我想为史蒂夫·杰索普(Steve Jessop)的答案添加一些内容(因为我没有足够的声誉,所以我无法对此发表评论)。但是我发现了一些有用的材料。他的回答很有帮助,但他犯了一个错误:存储桶大小不应该是2的幂。我仅引用第263页的Thomas Cormen,Charles Leisersen等人的“算法介绍”一书:
使用除法时,我们通常避免使用某些m值。例如,m不应该是2的幂,因为如果m = 2 ^ p,则h(k)只是k的p个最低位。除非我们知道所有低阶p位模式均等可能,否则我们最好将散列函数设计为依赖于密钥的所有位。正如练习11.3-3所要求的那样,当k是用基数2 ^ p解释的字符串时,选择m = 2 ^ p-1可能不是一个好选择,因为对k的字符进行置换不会改变其哈希值。
希望能帮助到你。
对于散列函数,不仅重要的是最大程度地减少细菌总数,而且在改变几个字节的同时使其无法保持相同的散列。
假设你有一个公式:
(x + y*z) % key = x
以0<x<key
和0<z<key
。如果key是素数,则n * y = key对于N中的每n个为true,而对于其他每个数字为false。
key不是主要示例的示例:x = 1,z = 2和key = 8因为key / z = 4仍然是自然数,所以4成为方程的解,在这种情况下为(n / 2) * y = N中的每n个键都为真。该方程的解数实际上增加了一倍,因为8不是质数。
如果我们的攻击者已经知道该方程式可能是8,那么他可以将文件从产生8更改为4并仍然获得相同的哈希值。
我已经阅读了流行的wordpress网站,这些网站在顶部列出了一些上述流行的答案。据我了解,我想分享一个简单的观察。
您可以在此处找到本文中的所有详细信息,但假定以下情况成立:
一般的hashmap实现需要2件事是唯一的。
我们如何获得唯一索引?通过使内部容器的初始尺寸也成为质数。因此,基本上涉及质数,因为质数具有产生唯一数字的独特特征,最终我们将其用于ID对象并在内部容器中查找索引。
例:
键=“键”
值=“值”
uniqueId = "k" * 31 ^ 2 +
"e" * 31 ^ 1` +
"y"
映射到唯一ID
现在我们想要一个独特的位置来实现我们的价值-因此我们
uniqueId % internalContainerSize == uniqueLocationForValue
,假设internalContainerSize
也是素数。
我知道这是简化的,但是我希望能使总体思路得以通过。
关于素数幂模的“数学性质”是它们是有限域的一个基本组成部分。其他两个构造块是加法和乘法运算。质数模的特殊性质是,它们与“常规”加法和乘法运算形成了一个有限域,刚好考虑了模数。这意味着每个乘法都以质数为模映射到一个不同的整数,每个加法也是如此。
质数模是有优势的,因为:
但是,它们有很大的缺点,它们需要整数除法,即使在现代CPU上,也需要许多(〜15-40)个周期。通过大约一半的计算,可以确保哈希混合得很好。两次乘法和异或移位运算将比主要模数混合得更好。然后,我们可以使用任何最快的哈希表大小和哈希减少方法,对于2种表大小的幂,总共可以进行7次操作,对于任意大小,总共可以进行9次操作。
我最近查看了许多最快的哈希表实现,其中大多数不使用素数模。
这个问题与一个更合适的问题合并,为什么哈希表应该使用素数数组,而不是2的幂。对于哈希函数本身,这里有很多好的答案,但是对于相关的问题,为什么有些安全性至关重要的哈希表,像glibc一样,使用素数数组,但还没有。
通常,2张桌子的功效要快得多。那里很贵h % n => h & bitmask
,可以通过clz
大小为n的(“前导零计数”)来计算位掩码。模函数需要做整数除法,比逻辑函数慢50倍and
。有一些技巧可以避免取模,例如使用Lemire的https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/,但是通常快速哈希表会使用幂2,安全哈希表使用素数。
为什么这样?
在这种情况下,安全性是通过对冲突解决策略的攻击来定义的,对于大多数哈希表而言,冲突表中的线性表只是线性搜索。或者使用更快的开放式地址表直接在表中进行线性搜索。因此,借助2张表的功能和对该表的一些内部知识(例如,某些JSON接口提供的键的大小或顺序),您可以获得使用的正确位数。位掩码上的1的数量。通常低于10位。对于5到10位,即使使用最强大和最慢的哈希函数,对于暴力冲突也是微不足道的。您再也无法获得32位或64位哈希函数的完全安全性。关键是要使用快速的小型哈希函数,而不要使用诸如杂音甚至是siphash之类的怪物。
因此,如果您为哈希表提供外部接口(例如DNS解析器,编程语言),那么您希望关心那些喜欢使用DOS这样的服务的人。对于这类人来说,通常更容易用更简单的方法来关闭您的公共服务,但这确实发生了。所以人们确实在乎。
因此,防止此类碰撞攻击的最佳选择是
1)使用素数表,因为
2)结合实际使用的2种大小的快速力量,使用更好的措施抵御实际攻击。
有一个广泛的神话,即更安全的哈希函数有助于防止此类攻击,这是错误的,正如我所解释的那样。仅低位就没有安全性。这仅适用于素数大小的表,但这将结合使用两种最慢的方法(慢散列加慢素数模)。
哈希表的哈希函数主要需要小(易于使用)和快速。安全只能来自防止冲突中的线性搜索。并且不要使用琐碎的哈希函数,例如对某些值不敏感的哈希函数(如使用乘法时为\ 0)。
使用随机种子也是一个不错的选择,人们首先会使用它,但是有了足够的表信息,即使是随机种子也无济于事,动态语言通常不容易通过其他方法来获取种子,因为它存储在已知的内存位置。
function eratosthenes(n) {
function getPrime(x) {
var middle = (x-(x%2))/2;
var arr_rest = [];
for(var j=2 ; j<=middle;j++){
arr_rest.push(x%j);
}
if(arr_rest.indexOf(0) == -1) {
return true
}else {
return false
}
}
if(n<2) {
return []
}else if(n==2){
return [2]
}else {
var arr = [2]
for(var i=3;i<n;i++) {
if(getPrime(i)){
arr.push(i)
}
}
}
return arr;
}