一、 核心原理:为什么叫“布谷鸟”?
布谷鸟(Cuckoo)有一种习性:它不筑巢,而是把蛋下在其他鸟的巢里。如果巢里的蛋满了,它会把别人的蛋踢出去,把自己的蛋放进去。
布谷鸟哈希借用了这个隐喻:
-
两个哈希函数:我们准备两个不同的哈希函数,$H_1$和$H_2$。
-
两个可能的巢:对于任何一个元素$x$,它只有两个可能的位置:$T_1[H_1(x)]$或者$T_2[H_2(x)]$。
-
踢出(Kick)机制:如果要插入新元素时,发现两个位置都被占了,新元素会强制把老元素“踢走”,霸占它的位置。被踢走的老元素则去寻找它的备用位置。
二、 配置表(构建)与查表流程
1. 查表原理(Lookup)
这是布谷鸟哈希最大的优势。查找过程极其简单,没有任何链表遍历或探测循环。
给定一个 Key,查找步骤如下:
-
计算位置A:$Pos_A = H_1(Key)$。检查该位置是否是我们要找的 Key?如果是,返回。
-
计算位置B:$Pos_B = H_2(Key)$。检查该位置是否是我们要找的Key?如果是,返回。
-
结束:如果这两个位置都不是,那么该 Key 一定不存在。
结论:查找次数固定为2 次哈希计算+ 2 次内存访问。绝对的$O(1)$。
2. 配置表生成原理(Insertion/Build)
构建过程相对复杂,因为涉及“踢馆”行为。
插入Key 的步骤:
-
先尝试放入位置$H_1(Key)$。如果该位置为空,插入成功。
-
如果位置$H_1(Key)$已经有元素(叫它$Old\_Key$),则新 Key 霸占该位置。
-
$Old\_Key$ 被踢出,被迫去寻找它的另一个位置(如果它之前在 $H_1$ 位置,它就去 $H_2$位置;反之亦然)。
-
如果$Old\_Key$ 的新位置又是空的,插入成功。
-
如果新位置也有人,继续踢出……以此类推,直到所有元素都安顿好。
-
死循环:如果踢出的次数超过设定阈值(说明表太满或形成环路),则判定构建失败,需要更换哈希函数的随机种子(Seed)并**重哈希(Rehash)**整个表。
三、 具体例子演练
假设我们有两个哈希表$T_1$和$T_2$,长度都为 5(索引 0-4)。
设定哈希函数:
-
H_1(x) = x mod 5
-
H_2(x) = (x / 5) mod 5(取整除5后的模)
我们要插入的数据序列: 20, 50, 53
第一步:插入20
-
计算$H_1(20) = 20 mod 5 = 0。
-
检查$T_1[0],是空的。
-
操作:直接放入。
| 索引 | T1(哈希 1) | T2(哈希 2) |
| 0 | 20 | |
| 1 | ||
| 2 | ||
| 3 | ||
| 4 |
第二步:插入50
-
计算$H_1(50) = 50 mod 5 = 0。
-
检查$T_1[0],冲突!里面已经有
20了。 -
踢出机制启动:
-
50霸占$T_1[0]。 -
20被踢出,手握着自己的值,去找备用位置。 -
20计算它的$H_2(20) = (20/5) mod 5 = 4。 -
检查$T_2[4],是空的。
-
20成功落户$T_2[4]。
-
| 索引 | T1(哈希 1) | T2(哈希 2) |
| 0 | 50 (把20踢走了) | |
| 1 | ||
| 2 | ||
| 3 | ||
| 4 |
第三步:插入53 (多米诺骨牌效应)
-
计算$H_1(53) = 53 mod 5 = 3。
-
检查$T_1[3],是空的。
-
操作:直接放入。
此时一切正常。为了演示更复杂的踢出,我们增加一个数据:插入75。
第四步:插入75 (复杂的踢出链)
-
计算$H_1(75) = 75 mod 5 = 0。
-
检查$T_1[0],冲突!里面是
50。 -
第一轮踢出:
-
75霸占$T_1[0]。 -
50被踢出。
-
-
50找新家:-
50原来在$H_1,现在找 $H_2。 -
$H_2(50) = (50/5) mod 5 = 10 mod 5 = 0。
-
检查$T_2[0],是空的。
-
50落户$T_2[0]。
-
最终状态:
| 索引 | T1(哈希 1) | T2(哈希 2) |
| 0 | 75 | 50 |
| 1 | ||
| 2 | ||
| 3 | 53 | |
| 4 | 20 |
四、 查表演示(Lookup)
现在我们要查找Key = 20。
-
计算Hash 1:$H_1(20) = 0。
-
查看$T_1[0],里面是
75。 -
不匹配 (75 不等于 20)。
-
-
计算Hash 2:$H_2(20) = 4。
-
查看$T_2[4],里面是
20。 -
匹配!返回数据。
-
如果要查找Key = 99 (不存在):
-
$H_1(99) = 4。$T_1[4]是空。
-
$H_2(99) = 4。$T_2[4]是
20(不匹配)。 -
结论:不存在。
五、 为什么配置表特别适合布谷鸟哈希?
配置表(如Excel 转出的二进制数据)有以下特点:
-
只读(Read-Only):程序运行时,我们只会疯狂查询,不会动态插入删除。
-
离线构建:我们可以在打包阶段(Build Time)花时间去生成哈希表。即使构建时发生了死循环(踢出次数过多),编译器只需要换一个随机种子重新生成一遍即可,直到生成一个完美的、没有冲突的表结构。
对比传统哈希(链地址法):
-
传统:如果发生冲突,会生成链表。查询时,最坏情况要遍历链表,缓存命中率(Cache Miss)高。
-
布谷鸟:查询最多跳两次内存,且没有指针跳转,对 CPU 缓存极其友好,查询效率极其稳定。
总结
-
配置:利用“踢出”机制解决冲突,像华容道一样移动元素直到大家都有位置。如果太拥挤移不动,就换个哈希种子重来。
-
查表:只看两个固定位置,一看便知,速度极快。