选择一个好的哈希表长度证明(为啥是素数)

Posted

技术标签:

【中文标题】选择一个好的哈希表长度证明(为啥是素数)【英文标题】:Choosing a good hash table length proof (why a prime)选择一个好的哈希表长度证明(为什么是素数) 【发布时间】:2021-09-02 15:00:56 【问题描述】:

我已经在 SO 和网络上阅读了几个关于选择一个好的哈希表长度的答案,它应该是减少冲突和在哈希表中均匀分布密钥的首要条件。

虽然有很多答案,但我找不到令人满意的证明,我不明白我找到的解释。

因此,如果我们有一个键 k 和一个长度为 n 的哈希表,并且我们执行 k % n = i 以在哈希表中找到一个桶的索引 i,我们说 n 应该是一个素数,以便最大限度地减少冲突次数并更好地在哈希表中分配密钥。

但是为什么呢?这是我试图证明这一点的尝试。会比较长,有点迂腐,但请耐心看完。

我将首先做出以下假设:

对于键集K 中的每个键k,我们可以有一个k,它可以是偶数也可以是奇数。键是一个整数,可以是偶数 (k = 2x) 或奇数 (k = 2x + 1)。 对于我们可以选择的每个nn 也可以是偶数 (n = 2y) 或奇数 (n = 2y + 1)。 如果我们将一个偶数加到另一个偶数上,我们会得到一个偶数 (2x + 2y = 2(x + y))。同样,如果我们将一个奇数添加到另一个奇数,我们仍然会得到一个偶数 ((2x + 1) + (2y + 1) = 2x + 1 + 2y + 1 = 2x + 2y + 2 = 2(x + y + 1))。 如果我们将奇数添加到偶数(与将偶数添加到奇数相同),我们总是会得到奇数 ((2x + 1) + 2y = 2x + 1 + 2y = 2(x + y) + 1)。

首先,让我们尝试考虑使用n,它不是素数,所以也许我们会发现这些数字不足以用作长度一个哈希表(假设键共享一些模式,例如全偶数或全奇数)。

    假设n 是偶数,即n = 2y。在这种情况下,我们有两种情况:K 的键 k 可以是偶数 (1.1.) 或奇数 (1.2.)。

1.1. n = 2y 是偶数,键是偶数 k = 2x

对于k = 2xn = 2y,我们有:k % n = 2x % 2y = i

在这种情况下,我们可以说如果键 k 和哈希表长度 n 都是偶数, 那么i 也将永远是平的。 为什么?因为如果我们通过整数除法k // n = 2x // 2y = q 得到商,我们会得到一个商q,这样:

k = 2x = (n * q) + i = (2y * q) + i = 2yq + i

由于2yq (2y * q) 是偶数,为了满足2x = 2yq + i 余数i 总是偶数,因为2x 是偶数(even + even = even)。如果 i 是奇数,我们会得到一个奇数 (even + odd = odd),但 2x 还是偶数。

如果我们选择n 是偶数,这会导致以下问题:如果我们所有的ks 都是偶数,那么它们将始终在一个偶数索引的桶中结束,从而增加了碰撞和聚类的数量因为只有一半 n / 2 的哈希表长度(只有偶数索引)会被占用。

因此,如果我们所有的ks 或我们的大多数ks 都将是偶数,那么对n 使用偶数不是一个好主意。

1.2. n = 2y 是偶数,键是奇数 k = 2x + 1

对于k = 2x + 1n = 2y,我们有:k % n = (2x + 1) % 2y = i。 同样,在这种情况下,如果我们所有的ks(或其中大多数)都是奇怪的,我们最终会遇到这种情况:

k = 2x + 1 = (n * q) + i = (2y * q) + i = 2yq + i

由于2yq 是偶数,为了得到奇数k = 2x + 1i 总是会是奇数 (even + odd = odd)。

同样,选择偶数 n 作为哈希表长度是一个坏主意,即使我们的所有或大部分 ks 都是奇数,因为我们最终只会占用奇数索引(存储桶) .

所以让我们尝试使用不是偶数的n,即奇数n = 2y + 1

    假设n 是奇数,即n = 2y + 1。我们仍然有偶数 (2.1.) 和奇数 (2.2.) 键(k of K)。

2.1. n = 2y + 1 是奇数,键是偶数 k = 2x

我们有:

k = 2x = (n * q) + i = ((2y + 1) * q) + i = (2yq + q) + i = 2yq + q + i

我们知道2yq 是偶数,所以为了得到同样偶数的k = 2x,我们需要q + i 也是偶数。 q + i 什么时候可以平?仅在这两种情况下:

    q -> even, i -> even, even + even = even q -> odd, i -> odd, odd + odd = even

如果qi 是偶数而另一个是奇数,我们将得到一个奇数q + i,因此得到一个奇数2yq + (q + i),但我们有k = 2x 这是偶数,所以要么qi 都是偶数或者都是奇数。

在这种情况下,我们可以看到对于奇数 n = 2y + 1i 可以是偶数或奇数,这很好,因为这意味着现在我们将使用哈希表的偶数和奇数桶索引,而不是只有偶数或只有奇数。

顺便说一句,所有素数 p : p > 2 都是奇数,所以至少现在我们可以说选择素数可能是个好主意,因为大于 2 的素数总是奇怪的。

2.2. n = 2y + 1 是奇数,键是奇数 k = 2x + 1

这里也一样:

k = 2x + 1 = (n * q) + i = ((2y + 1) * q) + i = 2yq + q + i = 2yq + (q + i)

为了得到一个奇数的k = 2x + 1,我们需要(q + i) 是奇数(2yq 是偶数),而且这只发生在这两种情况下:

    q -> even, i -> odd, even + odd = odd q -> odd, i -> even, odd + even = odd

我们再次证明,奇数是n 的更好选择,因为这样我们就有机会同时占用偶数和奇数桶的索引i

现在,我被困在这里了。这个证明和素数之间有联系吗?我如何继续这个证明来得出结论:素数p 比具有类似推理的通用奇数更好?

编辑:

所以我试着进一步推理一下。这是我想出的:

3.使用通用奇数 nk 共享一个公共因子 f

我们可以说,对于在k (k = f * x = fx) 和 n (n = f * y = fy) 之间共享的任何因子 f,我们最终得到一个 i = k % n 也共享该公共因子 @987654438 @。为什么?

再次,如果我们尝试计算k

k = fx = (n * q) + i = (fy * q) + i = fyq + i

然后:

k = fx = fyq + i

只有当且仅当i 也共享f 作为其因素之一时才能满足,例如i = f * g = fg:

k = fx = fyq + fg = f(yq + g)

导向yq + g = x

这意味着如果kn 共享一个公因数,则模i 的结果也将具有该公因数,因此i 将始终是该公因数的倍数,例如对于 kK = 12, 15, 33, 96, 165, 336n = 9(奇数,不是素数):

k    |  k % n
---------------------------
12   |  12 % 9 = 3
15   |  15 % 9 = 6
33   |  33 % 9 = 6
96   |  96 % 9 = 6
165  | 165 % 9 = 3
336  | 336 % 9 = 3

kn 总是共享一个公共因子(在这种情况下为 3)。 这导致i = k % n 也是3 的倍数,因此,在这种情况下,使用的哈希表的桶索引也只会是公因子3 的倍数。

因此,虽然 n 的奇数肯定比偶数好(如 2.1.2.2 中所述),但我们仍然可能在当kn 共享一个共同因子f 时的数字。

因此,如果我们将 n 设为素数 (n = p),我们肯定会避免 nk 共享公因数 f(前提是 f != p),因为素数 @ 987654471@只能有两个因素:1和它自己。所以...

4.为n 使用素数

如果 n 是素数 (n = p),我们最终得到:

k = fx = (q * p) + i = qp + i

然后:

k = fx = qp + i

暗示由整数除法k // n得到的商q可以共享或不共享公因数f,即:

    q = fz

或者:

    q = z

在第一种情况下 (q = fz) 我们有:

k = fx = (q * p) + i = (fz * p) + i = fzp + i

所以i 最终也共享共同因子f,例如i = fg:

k = fx = (q * p) + i = (fz * p) + i = fzp + i = fzp + fg = f(zp + g)

这样zp + g = x

在第二种情况下 (q = z),我们有:

k = fx = (q * p) + i = (z * p) + i = zp + i = zp + i

即在第二种情况下,i 不会将 f 作为其因子之一,因为 zp 也不会将 f 作为其因子之一。

因此,当对n 使用素数时,好处是i = k % n 的结果可以与k 共享一个公因数f,或者根本不共享它,例如对于kK = 56, 64, 72, 80, 88, 96n = p = 17

k    |  k % n
---------------------------
56   |  56 % 17 = 5
64   |  64 % 17 = 13
72   |  72 % 17 = 4 ---> Common factor f = 4 of k and i 
80   |  80 % 17 = 12 ---> Common factor f = 4 of k and i
88   |  88 % 17 = 3
96   |  96 % 17 = 11

在这种情况下,所有ks 共享一个公共因子f = 4,但只有i = 72 % 17 = 4i = 80 % 17 = 12 都有ki 共享该公共因子f

72 % 17 = 4 -> (18 * 4) % 17 = (4 * 1)
80 % 17 = 12 -> (20 * 4) % 17 = (4 * 3)

此外,如果我们采用前面的示例,对于 K = 12, 15, 33, 96, 165, 336k,我们使用素数 17 表示 n 而不是 9,我们得到:

k    |  k % n
---------------------------
12   |  12 % 17 = 12
15   |  15 % 17 = 15
33   |  33 % 17 = 16
96   |  96 % 17 = 11
165  | 165 % 17 = 12
336  | 336 % 17 = 13

即使在这里,我们也看到,在这种情况下,公因子 f = 3 仅在这 3 种情况下在 kn 之间共享:

12 % 17 = 12 -> (4 * 3) % 17 = (4 * 3)
15 % 17 = 15 -> (5 * 3) % 17 = (5 * 3)
165 % 17 = 12 -> (55 * 3) % 17 = (4 * 3)

这样,使用素数,发生冲突的概率降低了,我们可以更好地在哈希表中分布数据。

现在,如果k 是素数,或者至少是素数的倍数,会发生什么?我认为在这种情况下,哈希表的分布会更好,因为如果kn 都是素数或者k 是素数的倍数,它们之间就不会有任何公因数,前提是k 不是素数n 的倍数。

这就是我的结论为什么素数更适合哈希表的长度。

希望收到您对我理解该主题的方式的反馈和想法。

谢谢。

【问题讨论】:

@Alejandro 我不会说这个问题与密码学有关。你读完了吗?我没有提到任何关于密码学的内容。这只是关于使用素数作为哈希表数据结构背后的想法以及该推理背后的证据。 @Alejandro 我的问题与密码学没有任何关系,它可以相关,但它与数据结构有关(哈希表是一种数据结构)。看看这些问题:***.com/questions/18037909/…、***.com/questions/730620/how-does-a-hash-table-work、***.com/questions/7306316/b-tree-vs-hash-table。他们没有代码。我想你同意我的观点,编程不是关于代码,而是关于思考。代码是我们思想的具体结果。 @rossum 我知道。例如,Java 在其 hashCode 实现中使用素数 31,例如对于String。不过,这不是重点。许多关于哈希表的书籍、文章、学术论文也建议使用素数作为表的大小,否则我不会问这个问题。 理论上:是的。在实践中:没关系。通常,键空间的基数大于散列空间的范围,因此无论如何您都必须处理冲突。拥有良好的传播(并且没有明显的冲突)是第一个目标。 但是,如果您对n 使用素数并且您的密钥符合某种模式(例如,所有密钥都是某个因子f 的倍数),则将它们分散到不同存储桶的机会就会增加.另一方面,如果n 不是素数,则使用相同的桶的机会是公因数f 的倍数。 【参考方案1】:

当涉及到链式哈希表时,您几乎可以找到答案,尽管它可以用更少的词来编写:

数据通常具有模式。例如,内存地址的低位通常为零。 许多散列函数,尤其是非常流行的多项式散列函数,都是仅使用加法、减法和乘法构建的。所有这些操作都具有结果的最低 n 位仅取决于操作数的最低 n 位的属性,因此这些哈希函数也具有此属性。 如果您的表大小在最低 n 位中为零,并且最低 n 位中的数据都相同,并且您的哈希函数具有上述属性上面...那么您的哈希表将只使用其每 2n 个插槽中的一个。

有时我们通过选择一个奇数的哈希表大小来解决这个问题。素数大小更好,因为表大小的每个小因素都会导致哈希值的不同算术级数出现类似问题。

不过,有时我们会通过向哈希表本身添加一个额外的哈希步骤来解决这个问题——一个额外的哈希步骤将哈希的所有位混合在一起并防止此类问题。 Java HashMap 使用大小为 2N 的表,但会执行此额外的混合步骤以帮助覆盖所有插槽。

用于链接哈希表。

对于使用开放寻址来解决冲突的哈希表,通常需要选择素数表大小以确保探测方案最终会检查所有(或至少一半)插槽。这是为了保证表至少在(半)满之前可以正常工作。

【讨论】:

感谢您的回答。请检查我的编辑,我添加了一些额外的想法,我想我根据我最初的推理提出了一个证明。谢谢!

以上是关于选择一个好的哈希表长度证明(为啥是素数)的主要内容,如果未能解决你的问题,请参考以下文章

为啥哈希表扩展通常通过将大小加倍来完成?

哈希算法和哈希表的区别?

数据结构哈希表,求大神,急急急

什么是好的哈希函数?

证明与计算: 从加密哈希函数到一致性哈希

在 rabin-karp 滚动哈希中选择基数和模素数