【发布时间】:2021-09-02 15:00:56
【问题描述】:
我已经在 SO 和网络上阅读了几个关于选择一个好的哈希表长度的答案,它应该是减少冲突和在哈希表中均匀分布密钥的首要条件。
虽然有很多答案,但我找不到令人满意的证明,我不明白我找到的解释。
因此,如果我们有一个键 k 和一个长度为 n 的哈希表,并且我们执行 k % n = i 以在哈希表中找到一个桶的索引 i,我们说 n 应该是一个素数,以便最大限度地减少冲突次数并更好地在哈希表中分配密钥。
但是为什么呢?这是我试图证明这一点的尝试。会比较长,有点迂腐,但请耐心看完。
我将首先做出以下假设:
- 对于键集
K中的每个键k,我们可以有一个k,它可以是偶数也可以是奇数。键是一个整数,可以是偶数 (k = 2x) 或奇数 (k = 2x + 1)。 - 对于我们可以选择的每个
n,n也可以是偶数 (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 = 2x 和n = 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 + 1 和n = 2y,我们有:k % n = (2x + 1) % 2y = i。
同样,在这种情况下,如果我们所有的ks(或其中大多数)都是奇怪的,我们最终会遇到这种情况:
k = 2x + 1 = (n * q) + i = (2y * q) + i = 2yq + i
由于2yq 是偶数,为了得到奇数k = 2x + 1,i 总是会是奇数 (even + odd = odd)。
同样,选择偶数 n 作为哈希表长度是一个坏主意,即使我们的所有或大部分 ks 都是奇数,因为我们最终只会占用奇数索引(存储桶) .
所以让我们尝试使用不是偶数的n,即奇数n = 2y + 1。
- 假设
n是奇数,即n = 2y + 1。我们仍然有偶数 (2.1.) 和奇数 (2.2.) 键(kofK)。
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
如果q 或i 是偶数而另一个是奇数,我们将得到一个奇数q + i,因此得到一个奇数2yq + (q + i),但我们有k = 2x 这是偶数,所以要么q 和 i 都是偶数或者都是奇数。
在这种情况下,我们可以看到对于奇数 n = 2y + 1,i 可以是偶数或奇数,这很好,因为这意味着现在我们将使用哈希表的偶数和奇数桶索引,而不是只有偶数或只有奇数。
顺便说一句,所有素数 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.使用通用奇数 n 与 k 共享一个公共因子 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。
这意味着如果k 和n 共享一个公因数,则模i 的结果也将具有该公因数,因此i 将始终是该公因数的倍数,例如对于 k 的 K = {12, 15, 33, 96, 165, 336} 和 n = 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
k 和 n 总是共享一个公共因子(在这种情况下为 3)。
这导致i = k % n 也是3 的倍数,因此,在这种情况下,使用的哈希表的桶索引也只会是公因子3 的倍数。
因此,虽然 n 的奇数肯定比偶数好(如 2.1. 和 2.2 中所述),但我们仍然可能在当k 和n 共享一个共同因子f 时的数字。
因此,如果我们将 n 设为素数 (n = p),我们肯定会避免 n 与 k 共享公因数 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,或者根本不共享它,例如对于k 的K = {56, 64, 72, 80, 88, 96} 和n = 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 = 4 和i = 80 % 17 = 12 都有k 和i 共享该公共因子f:
72 % 17 = 4 -> (18 * 4) % 17 = (4 * 1)
80 % 17 = 12 -> (20 * 4) % 17 = (4 * 3)
此外,如果我们采用前面的示例,对于 K = {12, 15, 33, 96, 165, 336} 的 k,我们使用素数 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 种情况下在 k 和 n 之间共享:
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 是素数,或者至少是素数的倍数,会发生什么?我认为在这种情况下,哈希表的分布会更好,因为如果k 和n 都是素数或者k 是素数的倍数,它们之间就不会有任何公因数,前提是k 不是素数n 的倍数。
这就是我的结论为什么素数更适合哈希表的长度。
希望收到您对我理解该主题的方式的反馈和想法。
谢谢。
【问题讨论】:
-
@Alejandro 我不会说这个问题与密码学有关。你读完了吗?我没有提到任何关于密码学的内容。这只是关于使用素数作为哈希表数据结构背后的想法以及该推理背后的证据。
-
@Alejandro 我的问题与密码学没有任何关系,它可以相关,但它与数据结构有关(哈希表是一种数据结构)。看看这些问题:stackoverflow.com/questions/18037909/…、stackoverflow.com/questions/730620/how-does-a-hash-table-work、stackoverflow.com/questions/7306316/b-tree-vs-hash-table。他们没有代码。我想你同意我的观点,编程不是关于代码,而是关于思考。代码是我们思想的具体结果。
-
@rossum 我知道。例如,Java 在其
hashCode实现中使用素数31,例如对于String。不过,这不是重点。许多关于哈希表的书籍、文章、学术论文也建议使用素数作为表的大小,否则我不会问这个问题。 -
理论上:是的。在实践中:没关系。通常,键空间的基数大于散列空间的范围,因此无论如何您都必须处理冲突。拥有良好的传播(并且没有明显的冲突)是第一个目标。
-
但是,如果您对
n使用素数并且您的密钥符合某种模式(例如,所有密钥都是某个因子f的倍数),则将它们分散到不同存储桶的机会就会增加.另一方面,如果n不是素数,则使用相同的桶的机会是公因数f的倍数。
标签: data-structures hash hashtable primes hash-function