void hashTest2(int m, long A, long w) {
int M = (int)Math.pow(2, m);
String sq = "";
for (int K = 1; K <= M; K++) {
// compute h(K)
long h = ((A * K) % w) >>> (32 - m);
sq += h + " ";
}
System.out.println(sq);
}
// 执行
hashTest2(3, 2654435769L, (long)Math.pow(2, 32));
hashTest2(4, 2654435769L, (long)Math.pow(2, 32));
hashTest2(5, 2654435769L, (long)Math.pow(2, 32));
还有一些其它的斐波拉契散列变种,目的是为了进一步减少冲突。比如将移位等价法稍作改造,在计算出 a' = A*K%w 之后,不取 a' 的高 m 位,而是取其低 m 位。这样还有一个好处,即只需计算 a' = A*K,不用再进行求模运算了,因为求模本质上就是取低位部分的二进制位,所以不管求不求模,最终取到的低位部分都是一样的。
仍然以上一节的例子来进行验证:
void hashTest3(int m, long A) {
int M = (int)Math.pow(2, m);
String sq = "";
for (int K = 1; K <= M; K++) {
// compute h(K),取低 m 位
long h = (A * K) & (M - 1);
sq += h + " ";
}
System.out.println(sq);
}
// 执行
hashTest3(3, 2654435769L);
hashTest3(4, 2654435769L);
hashTest3(5, 2654435769L);
hashTest3(6, 2654435769L);
目录
1 乘法散列[Top]
斐波拉契散列是 乘法散列 的一种特例。乘法散列的散列函数表示如下:
其中,
K
是键值,M
是散列规模或散列范围,所以应有:0 ≤ h(K) < M
,通常 M 在一台二进制计算机上是 2 的乘方。A
是一个常数,ω
是计算机字的大小,比如在 32 位机上 ω=2^32,A 与 ω 互素。mod 1
表示取小数部分的值。假设 M=2^m,那么 h(K) 的计算过程就为:
为了避免低效率计算浮点数,上述过程可以等价为如下步骤:
2 斐波拉契散列[Top]
在实际应用中,一般取
A/ω
为 黄金分割比:A/ω = 1/φ = (sqrt(5) - 1) / 2 = 0.6180339887
。所以在 32 位机器上,A = (sqrt(5) - 1) / 2 × 2^32 ≈ 2654435769
。在这种情况下计算得到的散列值将均匀地分布在散列规模 M 内,即最大程度地减少发生冲突的可能性。我们称这样的散列方法为 斐波拉契散列。之所以叫斐波拉契散列,是因为选定的黄金分割比 1/φ 与斐波拉契数列有着如下密切的关系:斐波拉契数列中前后两项数的比值无限接近于黄金分割比 1/φ。这点可以从斐波拉契数列的通项公式得到:
可以直观感受如下。对于斐波拉契数列:1 1 2 3 5 8 13 21... 有:
作为例子,我们设:
m=3,4,5; M=2^m
,ω=2^32
,则A=0.6180339887×2^32=2654435769
。先按常规乘法散列方法计算:
执行结果为:
按照移位等价方法计算如下:
执行结果为:
两种方法得到的散列结果一样,且散列效果比较理想(只在 m=5 的时候出现了两次冲突)。
3 斐波拉契散列改造[Top]
还有一些其它的斐波拉契散列变种,目的是为了进一步减少冲突。比如将移位等价法稍作改造,在计算出
a' = A*K%w
之后,不取 a' 的高 m 位,而是取其低 m 位。这样还有一个好处,即只需计算a' = A*K
,不用再进行求模运算了,因为求模本质上就是取低位部分的二进制位,所以不管求不求模,最终取到的低位部分都是一样的。仍然以上一节的例子来进行验证:
执行结果为:
可以看到,在
m=3,4,5,6
时,散列结果都没有产生冲突(但并不代表在所有情况下都没有冲突)同时,注意
2654435769L
的二进制表示为:关注低 32 位,在 java 中若用 int 来表示该二进制,则最高位为 1 是一个负数,该二进制序列实际上是补码表示,那么其原码表示应为:
即十进制的
-1640531527
(也即(int)2654435769L
进行强制转换后的溢出结果)。而实际上我们也可以用
-1640531527
来代替2654435769L
达到同样的目的。进一步,在上述散列方法中,进行乘法和移位运算过程中始终不必关心符号位,所以以下两个二进制序列对散列结果的计算没有影响:
注意到,在 java ThreadLocal散列表中,其散列方法中用到的魔数 A 就是
0x61c88647
。其散列计算如下:
执行结果为:
4 原理初探[Top]
在 Knuth 的分析中,给出了一个有趣的事实,对于黄金分割比
1/φ
来说,设{x}
表示 x 的小数部分。那么在线段[0...1]
中,逐次标注点{1/φ}, {2×1/φ}, {3×1/φ},...
,则每个新增加的点都会落入最大剩余区间之一,且按照黄金分割比来分割这个最大剩余区间。如下图所示:此外,我们还注意到,如果分别计算出
[1,2,...]×魔数A
,且并列列出这些结果的低 32 位二进制序列,就能发现,除了取低位作为散列结果以外,取其它位置的的部分二进制位作为散列结果,也能得到较好的散列效果:图中框出了较好散列效果的列。
实际上,一个较好的经验是,将机器字长能表示的最大整数乘以黄金分割比得到一个魔数 A,然后按照乘法散列的方法来计算出散列结果。因为我们可以看到,魔数 A 的连续倍数在很多指定的二进制序列范围内分布是比较均匀的,这样做的结果是可以尽可能的减少冲突发送的概率。
5 参考[Top]
Knuth 计算机程序设计艺术第 6.4 节散列