从生日悖论看哈希函数的冲突问题
在讨论这个问题之前,先回顾一下哈希表是什么
1. 哈希表是什么?
哈希表(Hash Table)是一种 键–值对存储结构。
形象理解:
👉 就像一个“大柜子”,有很多小格子(桶)。
👉 你把名字(键)交给管理员(哈希函数),管理员告诉你“放到第 17 号格子”。
👉 下次再来找,只要去第 17 号格子就能拿到对应的值。
2. 哈希函数是什么?
哈希函数(Hash Function)就是一个 把任意输入映射到固定范围整数 的函数。
常见形式:
h(k)=kmod mh(k) = k \mod mh(k)=kmodm
(对整数键取模)
好的哈希函数要满足:
均匀性:让键尽可能分布均匀
确定性:相同输入必然映射到相同输出
高效性:计算要快
3. 哈希表的优势
查找、插入、删除平均时间复杂度为 O(1)
实现简单,底层就是数组 + 哈希函数
4. 哈希表的问题:冲突
因为桶的数量有限,不同的键可能映射到同一个位置,这叫 冲突 (Collision)。
常见的解决冲突的方法有:
链地址法(Chaining)开放寻址法(Open Addressing)双重哈希 / 再哈希Cuckoo Hashing
什么是生日悖论
最简单的想法就是 每个人的生日不相同的概率与直觉相违背。
比如:
我们知道 367 个人里必然有两个人同生日;
但很少人知道其实 23 个人时概率就超过 50%;
当人数达到 100 人时概率接近 99.997%。
生日悖论(Birthday Paradox) 指的是这样一个现象:
在只有 23 个人的群体里,至少有两个人生日相同的概率就超过 50%;当人数达到 100 时,这个概率几乎是 100%。
之所以叫“悖论”,是因为直觉认为要接近 365 人才会撞生日,但实际上由于 人数一多,两两比较的组合数急剧增加,重复的可能性比想象大得多。
数学推导(不计闰年)
设房间里有n个人,一年有m=365天。
所有人互不相同的概率:
Pdistinct(n)=m!(m−n)! mn
P_{\text{distinct}}(n)=\frac{m!}{(m-n)!\,m^n}
Pdistinct(n)=(m−n)!mnm!
至少两人同生日的概率:
Pcollision(n)=1−Pdistinct(n)
P_{\text{collision}}(n) = 1 - P_{\text{distinct}}(n)
Pcollision(n)=1−Pdistinct(n)
关键在于这个排列数,增长速度是飞快的。
下面给出一个python模拟代码可以看看这个小例子:
import random
from collections import Counterdef simulate_birthdays(n_people=100, n_days=365):# 随机生成 n_people 个生日(取值 1 到 n_days)birthdays = [random.randint(1, n_days) for _ in range(n_people)]return birthdaysdef has_duplicate(birthdays):c = Counter(birthdays)# 如果有任何一个生日出现次数 > 1,就说明有重复return any(count > 1 for count in c.values()), cdef main(trials=10000):duplicate_count = 0for _ in range(trials):bb = simulate_birthdays()dup, cnt = has_duplicate(bb)if dup:duplicate_count += 1print(f"在 {trials} 次模拟中,至少一对生日相同的频率是 {duplicate_count/trials:.6f}")# 这里再展示一次单次模拟的具体生日分布example = simulate_birthdays()print("一次模拟生成的 100 人生日样本如下:")print(sorted(example))if __name__ == "__main__":main()
如何估计生日悖论概率超过50%的数量?
事件:在 nnn 次独立投掷到 mmm 个等概率“格子”里,出现 至少一对碰撞(两次落同格)。
用泊松/指数近似:
P(碰撞)≈1−exp (−n(n−1)2m) P(\text{碰撞}) \approx 1-\exp\!\Big(-\tfrac{n(n-1)}{2m}\Big) P(碰撞)≈1−exp(−2mn(n−1))
令 P(碰撞)=0.5P(\text{碰撞})=0.5P(碰撞)=0.5,得门槛 n50n_{50}n50:
n50 ≈ 2mln2 = 1.1774 m n_{50} \;\approx\; \sqrt{2m\ln 2} \;=\; 1.1774\,\sqrt{m} n50≈2mln2=1.1774m
-
生日问题:m=365m=365m=365
n50≈1.1774365≈22.5 n_{50}\approx 1.1774\sqrt{365}\approx 22.5 n50≈1.1774365≈22.5
四舍五入就是 23。 -
bbb 位哈希:m=2bm=2^bm=2b
n50≈1.1774⋅2b/2 n_{50}\approx 1.1774\cdot 2^{b/2} n50≈1.1774⋅2b/2例:64 位哈希
n50≈1.1774⋅232≈5.05×109 n_{50}\approx 1.1774\cdot 2^{32}\approx 5.05\times 10^9 n50≈1.1774⋅232≈5.05×109
简单来说,对于哈希函数来说,虽然可以增加很多键值对内存空间,但是当处于sqrt(n)时,冲突的概率就会超过50%,这样就是冲突的来源。
我们可以通过构建双重哈希的方式避免一些冲突问题,即把哈希的键再当成值,建立了新的哈希表这样。