散列函数为什么要使用素数模数?

时间:2009-07-17 19:30:05

标签: language-agnostic data-structures hash

很久以前,我以1.25美元的价格在交易台上买了一本数据结构书。在其中,哈希函数的解释说,由于“数学的本质”,它最终应该由质数修改。

你对一本1.25美元的书有什么期望?

无论如何,我多年来一直在思考数学的本质,但仍然无法弄明白。

当存在素数时,数字的分布是否真的更均匀?或者这是一个老程序员的故事,每个人都接受,因为每个人其他接受它?

17 个答案:

答案 0 :(得分:232)

通常,一个简单的散列函数通过获取输入的“组成部分”(字符串的情况下的字符),并将它们乘以某个常量的幂,并将它们以某种整数类型加在一起来工作。因此,例如字符串的典型(尽管不是特别好)散列可能是:

(first char) + k * (second char) + k^2 * (third char) + ...

然后,如果输入一堆具有相同第一个字符的字符串,则结果将全部为模k,至少直到整数类型溢出为止。

[例如,Java的字符串hashCode与此类似 - 它的字符顺序相反,k = 31。因此,在以相同方式结束的字符串之间获得以模数31为模型的醒目关系,以及在除了接近结尾之外相同的字符串之间以2 ^ 32为模数的醒目关系。这并不会严重破坏散列表行为。]

哈希表的工作原理是将哈希模数乘以桶数。

在哈希表中,重要的是不要为可能的情况产生冲突,因为冲突会降低哈希表的效率。

现在,假设有人将一大堆值放入哈希表中,这些哈希表中的项之间存在某种关系,就像所有具有相同的第一个字符一样。我会说,这是一种相当可预测的使用模式,因此我们不希望它产生太多的冲突。

事实证明“由于数学的性质”,如果哈希中使用的常量和桶的数量是coprime,那么在一些常见情况下碰撞会被最小化。如果它们不是coprime,则输入之间存在一些相对简单的关系,其中碰撞未被最小化。所有的哈希值都与公共因子相等,这意味着它们都将落入具有以公共因子为模的值的桶的第1个中。你得到n次碰撞,其中n是公因子。因为n至少为2,所以我认为一个相当简单的用例产生至少两倍于正常情况的冲突是不可接受的。如果某个用户打算将我们的版本分配到存储桶中,我们希望它是一个奇怪的事故,而不是一些简单的可预测用法。

现在,散列表实现显然无法控制放入它们的项目。他们不能阻止他们相关。所以要做的是确保常量和桶数是互质的。这样,您不仅仅依靠“最后”组件来确定铲斗的模数与一些小的公因数。据我所知,他们没有必要成为实现这一目标的首要任务,只需互质。

但是如果哈希函数和哈希表是独立编写的,那么哈希表不知道哈希函数是如何工作的。它可能使用一个小因子的常数。如果你很幸运,它可能完全不同并且是非线性的。如果哈希足够好,那么任何桶数都可以。但是一个偏执的哈希表不能假设一个好的哈希函数,所以应该使用素数桶。类似地,偏执散列函数应该使用一个较大的素数常量,以减少某人使用多个桶的机会,这些桶碰巧与常量具有共同因子。

在实践中,我认为使用2的幂作为桶的数量是相当正常的。这是方便的,并且节省了必须搜索或预先选择正确幅度的素数。因此,您依赖哈希函数不使用偶数乘数,这通常是一个安全的假设。但是你仍然可以根据上面的散列函数偶尔得到糟糕的散列行为,而且主数据桶计数可能会有所帮助。

关于“一切都必须是素数”的原则,据我所知,在哈希表上进行良好分配是一个充分但不是必要的条件。它允许每个人进行互操作,而无需假设其他人遵循相同的规则。

[编辑:使用素数桶的另一个更专业的理由是,如果你处理线性探测的碰撞。然后你从哈希码计算一个步幅,如果那个步幅是桶数的一个因子,那么你只能在你回到起点之前做(bucket_count / stride)探测。你最想避免的情况是stride = 0,当然,这必须是特殊的,但是为了避免特殊套管bucket_count / stride等于一个小整数,你可以只做一个bucket_count prime而不关心什么提供步幅不是0。]

答案 1 :(得分:28)

从哈希表插入/ retreiving时,首先要做的是计算给定键的hashCode,然后通过hashCode%table_length将hashCode修剪为hashTable的大小来找到正确的桶。以下是您最有可能在某处阅读过的2个“陈述”

  1. 如果对table_length使用2的幂,则查找(hashCode(key)%2 ^ n)与(hashCode(key)&(2 ^ n -1))一样简单快捷。但是如果你为给定键计算hashCode的函数不好,你肯定会在几个散列桶中聚集许多键。
  2. 但是如果你使用素数作为table_length,那么即使你有一个稍微愚蠢的hashCode函数,计算出来的hashCodes也可以映射到不同的散列桶中。
  3. 这是证据。

    如果假设你的hashCode函数导致以下hashCodes {x,2x,3x,4x,5x,6x ...},那么所有这些都将集中在m个桶中,其中m = table_length / GreatestCommonFactor(table_length,x)。 (验证/得出这个是微不足道的)。现在,您可以执行以下操作之一以避免群集

    确保你没有生成太多的hashCode,这些hashCode是{x,2x,3x,4x,5x,6x ...}中的另一个hashCode的倍数。但是如果你的hashTable是,那么这可能有点困难应该有数百万条目。 或者通过使GreatestCommonFactor(table_length,x)等于1来简单地使m等于table_length,即通过使table_length与x进行互操作。如果x可以是任何数字,那么请确保table_length是素数。

    从 - http://srinvis.blogspot.com/2006/07/hash-table-lengths-and-prime-numbers.html

答案 2 :(得分:10)

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

非常明确的解释,也有图片。

编辑:作为摘要,使用素数是因为当您将值乘以所选的素数并将它们全部加起来时,您最有可能获得唯一值。例如,给定一个字符串,将每个字母值与素数相乘,然后将它们全部添加将为您提供其哈希值。

更好的问题是,为什么31号呢?

答案 3 :(得分:9)

TL;博士

index[hash(input)%2]会导致一半可能的哈希值和一系列值发生冲突。 index[hash(input)%prime]导致所有可能的哈希值中的< 2的冲突。将除数固定为表大小也可确保数字不能大于表格。

答案 4 :(得分:8)

使用Primes是因为您很有可能获得使用模数为P的多项式的典型哈希函数的唯一值。 比如说,你对长度为< = N的字符串使用这种哈希函数,并且你有一个碰撞。这意味着2个不同的多项式产生相同的模P值。这些多项式的差异也是相同度数N(或更小)的多项式。它具有不超过N个根(这里是数学的本质显示自己,因为这个说法仅适用于字段上的多项式=>素数)。因此,如果N远小于P,则可能不会发生碰撞。之后,实验可能会显示37大到足以避免长度为5-10的字符串哈希表的冲突,并且足够小以用于计算。

答案 5 :(得分:5)

只是为了提供另一个观点,就是这个网站:

http://www.codexon.com/posts/hash-functions-the-modulo-prime-myth

其中认为您应该使用最大数量的存储桶,而不是向下舍入到素数桶。这似乎是一种合理的可能性。直觉上,我当然可以看到更多数量的桶会更好,但我无法对此进行数学论证。

答案 6 :(得分:3)

  

Primes是唯一的数字。他们是   独一无二的,一个素数的产物   与任何其他数字有最好的   独特的机会(不是唯一的   因为当然的主要原因   素数习惯的事实   撰写它。此属性用于   散列函数。

     

给出一个字符串“塞缪尔”,你可以   通过乘法生成唯一的哈希   每个组成数字或   带素数的字母和添加   他们了这就是使用素数的原因。

     

然而,使用素数是一个旧的   技术。关键在这里要了解   只要你能生成一个   你可以移动的足够独特的钥匙   其他散列技术也是如此。走   这里有关于此主题的更多信息   http://www.azillionmonkeys.com/qed/hash.html

http://computinglife.wordpress.com/2008/11/20/why-do-hash-functions-use-prime-numbers/

答案 7 :(得分:3)

这取决于哈希函数的选择。

许多散列函数将数据中的各种元素与一些因子相乘,并将两个函数的模数乘以与机器字大小相对应的模数(通过让计算溢出来获得模数是免费的)。

您不希望数据元素的乘数与散列表的大小之间存在任何公因子,因为这样可能会发生变化数据元素不会将数据分散到整个表中的情况。如果您选择表格大小的素数,则这种公共因素极不可能。

另一方面,这些因素通常由奇数素数组成,因此对于哈希表使用2的幂也应该是安全的(例如,Eclipse在生成Java hashCode()方法时使用31)。

答案 8 :(得分:2)

假设您的表大小(或模数)是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也很糟糕。

因此,素数倾向于给出更好的分布结果,因此是表格大小的好选择。

答案 9 :(得分:2)

  

从我的其他答案https://stackoverflow.com/a/43126969/917428复制。请参阅它以获取更多详细信息和示例。

我认为这只与计算机在基础2中工作这一事实有关。只要想想基础10的同样的东西是如何工作的:

  • 8%10 = 8
  • 18%10 = 8
  • 87865378%10 = 8

数字是什么并不重要:只要它以8结尾,其模10将为8。

选择一个足够大的非二次幂数将确保散列函数确实是所有输入位的函数,而不是它们的一个子集。

答案 10 :(得分:1)

我想为Steve Jessop的回答添加一些东西(我不能评论它,因为我没有足够的声誉)。但我发现了一些有用的材料。他的回答非常有帮助,但是他犯了一个错误:桶的大小不应该是2的幂。我只是引用这本书"算法简介"作者:Thomas Cormen,Charles Leisersen等人,第263页:

  

使用除法时,我们通常会避免使用m的某些值。例如,m不应该是2的幂,因为如果m = 2 ^ p,则h(k)只是k的p个最低位。除非我们知道所有低阶p比特模式都是同等可能的,否则我们最好设计散列函数以依赖于密钥的所有比特。正如练习11.3-3要求你展示的那样,当k是以基数2 ^ p解释的字符串时,选择m = 2 ^ p-1可能是一个糟糕的选择,因为置换k的字符不会改变其散列值。 / p>

希望它有所帮助。

答案 11 :(得分:0)

对于哈希函数来说,不仅要尽量减少一般的聚合,而且要在查看几个字节时不能使用相同的哈希值。

说你有一个等式: (x + y*z) % key = x 0<x<key0<z<key。 如果key是一个素数n * y = key对于N中的每个n都为真,而对于每个其他数字都为false。

密钥不是主要示例的示例: x = 1,z = 2且key = 8 因为key / z = 4仍然是一个自然数,4成为我们方程的解,在这种情况下(n / 2)* y = key对于N中的每个n都为真。方程的解的数量实际上加倍因为8不是素数。

如果我们的攻击者已经知道8可能是等式的解决方案,他可以将文件从生成8改为4并仍然获得相同的散列。

答案 12 :(得分:0)

我已经阅读了热门的wordpress网站,该网站在顶部的一些上述流行答案中链接。根据我的理解,我想分享一个简单的观察结果。

您可以在文章here中找到所有详细信息,但假设以下情况属实:

  • 使用素数为我们提供唯一值
  • 的“最佳机会”

一般的hashmap实现需要两件事是唯一的。

  • 的唯一哈希码
  • 唯一索引,用于存储实际的

我们如何获得唯一索引?通过使内部容器的初始尺寸成为主要原因。所以基本上,涉及素数是因为它具有产生唯一数字的这一独特特征,我们最终将其用于ID对象并在内部容器内查找索引。

示例:

key =“key”

value =“value” Sub mm) With Intersect(ActiveSheet.UsedRange, Range("B:H")) .SpecialCells(XlCellType.xlCellTypeBlanks).FormulaR1C1 = "=IFERROR(INDEX(Sheet1!R2C2:R100C7,MATCH(RC1,Sheet1!R2C1:R100C1,0),MATCH(R1C,Sheet1!R1C2:R1C7,0)),""Not Found"")" .Value = .Value .Replace what:="Not Found", replacement:="", lookat:=xlWhole End With End Sub

映射到唯一ID

现在我们想要一个唯一位置作为我们的价值 - 所以我们

uniqueId = "k" * 31 ^ 2 + "e" * 31 ^ 1` + "y" ,假设uniqueId % internalContainerSize == uniqueLocationForValue也是素数。

我知道这是简化的,但我希望通过这个概念。

答案 13 :(得分:0)

关于素数幂模的“数学性质”是它们是finite field的组成部分。其他两个构造块是加法和乘法运算。质数模的特殊性质是,它们与“常规”加法和乘法运算形成了一个有限域,刚好取模数。这意味着每个乘法都以质数为模映射到一个不同的整数,每个加法也是如此。

主要模数是有利的,因为:

  • 在二级哈希中选择二级乘法器时,它们提供了最大的自由度,除0以外的所有乘法器最终将只访问一次所有元素
  • 如果所有哈希值均小于模数,则根本不会发生冲突
  • 随机质数混合比两个模的幂更好,并且压缩所有比特的信息而不仅仅是子集

但是,它们有很大的缺点,它们需要整数除法,即使在现代CPU上,也需要很多(〜15-40)个周期。通过大约一半的计算,可以确保哈希混合得很好。两次乘法和xorshift运算将比主要模数混合得更好。然后,我们可以使用任何哈希表大小和哈希减少最快的方法,对于2种表大小的幂,总共可以进行7个操作,而对于任意大小,总共可以进行9次操作。

我最近查看了许多fastest hash table implementations,其中大多数没有使用素数模量。

答案 14 :(得分:0)

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;

}

答案 15 :(得分:0)

此问题与更合适的问题合并,为什么哈希表应使用素数数组而不是2的幂。 对于哈希函数本身,这里有很多好的答案,但是对于相关的问题,为什么某些对安全性要求很高的哈希表(例如glibc)使用素数数组,还没有。

通常,两张桌子的功效要快得多。这里有昂贵的h % n => h & bitmask,可以通过大小为n的clz(“前导零计数”)来计算位掩码。模函数需要做整数除法,比逻辑and慢约50倍。有一些技巧可以避免取模,例如使用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)使用主要表,因为那样

  • 所有32位或64位都与查找存储区相关,而不仅仅是少数。
  • 哈希表调整大小功能比仅使用double更为自然。最好的增长函数是斐波那契数列,素数比加倍数更接近。

2)使用更好的措施抵御实际攻击,同时具有2种大小的快速威力。

  • 计算碰撞次数,并在检测到的攻击中中止或睡眠,这是碰撞次数<1%。像100个带有32位哈希表的表。这就是例如djb的dns解析器可以。
  • 当检测到碰撞攻击时,使用O(log n)搜索而不是O(n)将冲突的链接列表转换为树。这就是例如Java。

有一个广泛的神话,即更安全的哈希函数有助于防止此类攻击,这是错误的,正如我所解释的那样。仅低位就没有安全性。这仅适用于素数大小的表,但这将结合使用两种最慢的方法(慢散列加慢素数模)。

哈希表的哈希函数主要需要小(易于使用)和快速。安全只能来自防止冲突中的线性搜索。而且不要使用琐碎的哈希函数,例如对某些值不敏感的哈希函数(如使用乘法时为\ 0)。

使用随机种子也是一个不错的选择,人们首先开始使用它,但是有了足够的表信息,即使是随机种子也无济于事,动态语言通常不容易通过其他方法来获取种子,例如它存储在已知的内存位置。

答案 16 :(得分:0)

我想说this link的第一个答案是我找到的关于这个问题的最清晰答案。

考虑键集 K = {0,1,...,100} 和哈希表,其中存储桶数为 m = 12 。由于 3 12 的因数,因此 3 倍数的键将被散列到 3 < / strong>:

  • {0,12,24,36,...} 将散列到存储区0。
  • {3,15,27,39,...} 将散列到存储区3。
  • {6,18,30,42,...} 将散列到存储桶6。
  • {9,21,33,45,...} 将散列到存储桶9。

如果 K 是均匀分布的(即, K 中的每个键均可能发生),那么m的选择不是那么关键。但是,如果 K 分布不均匀怎么办?想象一下,最可能出现的键是 3 的倍数。在这种情况下,所有不是 3 倍数的存储桶都将很可能为空(这在哈希表性能方面确实很糟糕)。

这种情况看起来更常见。例如,想象一下,您正在根据对象在内存中的存储位置来跟踪它们。如果计算机的字长为4个字节,则您将使用 4 的倍数作为哈希键。不用说,将m选为 4 的倍数将是一个糟糕的选择:您将 3m / 4 存储桶完全清空,并且所有键都在剩余的 m / 4 个桶。

通常:

K中与存储桶数m共有一个公因数的每个键都将散列到该存储桶数的倍数。

因此,为了最大程度地减少冲突,重要的是减少m和 K 的元素之间的公因数。如何做到这一点?通过选择m为具有很少因素的数字:素数

通过Mario来回答问题。