哈希函数是一种将任意长度的输入通过散列算法变换成固定长度的输出的函数,当不同键的哈希值相同,就会发生Hash碰撞
解决这个问题的思路有两个,一个是发生碰撞后解决碰撞,另一个是避免碰撞
发生碰撞后解决碰撞的方法有开放寻址法和拉链法,这些网上都能搜到,就不赘述了
下面要讨论的是通过素数来减少哈希函数的碰撞
1.表的大小为质数减少哈希碰撞
哈希函数的关键属性是它该是抗碰撞的,即为很难找到两个不同的输入产生出相同的输出,下面给出三种常用哈希函数中质数的应用
表的大小与特定输入数列的关系
当设计哈希表时,选择一个质数作为数组的大小可以减少键值散列到相同索引位置(即发生碰撞)的概率
比如说有两个哈希表一个为100(合数),一个为97(素数)
当输入的数据是一系列以100的因子作为每一个数字的差值的数列的时候
比如说:5,15,25,35,45,55,65,75,85,95,105 ...
这时候哈希映射的计算方法为取余数
f(key) = (f(key)+di) % m (di=1,2,3,......,m-1)
使用100作为哈希表的大小时候
映射的结果为:
- 0 % 100 = 0
- 5 % 100 = 5
- 10 % 100 = 10
- 15 % 100 = 15
- ...
- 100 % 100 = 0
- 105 % 100 = 5
- 110 % 100 = 10
可见,在5和105的计算结果都是5,在100(第20个数)的时候发生了第一次碰撞,后面都是碰撞
使用97作为哈希表的大小时候
- 0 % 97 = 0
- 5 % 97 = 5
- 10 % 97 = 10
- 15 % 97 = 15
- ...
- 485 % 97 = 0
- 490 % 97 = 5
.到485(第97个数)的时候发生了第一次碰撞,即为 5×97=485
第一次发生碰撞的公式
发生第一次碰撞位置的公式推导:
对于任意步长 d和哈希表大小 m
①如果 d 是 m 的因数,那么第一次碰撞发生在:
第m/d个数字位置
②如果 d 不是 m 的因数,那么第一次碰撞发生在:
第m个数字位置
结合一下,第一次发生碰撞的位置为m/gcd(d,m)
gcd(d,m)是d和m的最大公约数
结论
当输入数列的步长为哈希表大小的因数的时候,更容易发生碰撞
因数越多,满足上述条件步长的情况越多
那么直接用个质数就能从一定程度上避免这种情况的发生
2.二次探测再散列中使用质数
使用线性探测法的时候,一旦某个区域开始变得密集,那么新元素插入时就需要更长的时间去寻找空槽,二次探测法则可以避免这种情况,因为它能够以不同的间隔跳跃式地探索散列表,从而使得插入的新元素更加均匀地分布在整个表中
f(key) = (f(key)+di) % m (di = 1^2, 2^2, 3^2,……, q^2, q <= m/2)
例如,初始位置为h(k),如果该位置已被占用,则尝试
(f(key) + 1^2)%m
(f(key) + 2^2)%m
(f(key) + 3^2)%m
...
二次探查使用的增量是平方数的形式,比如 i^2, 2i^2, 3i^2 等等。这样的探测序列意味着在某些情况下,特别是当哈希表大小不是质数时,可能会导致某些槽位永远不会被探测到,从而降低了表的有效容量,选择合适的表大小(通常是质数)可以减轻这个问题
如果选择表大小 m 为素数,那么二次探测至少会在重复前访问表的一半条目
推导过程:
假设存在两个探测次数i和j(i ≠ j),设定 i 和 j 在[0, m-1]中,使得它们探测到的位置相同,即
(hash(key) + i²) % m = (hash(key) + j²) % m
=》 (i² - j²) % m = 0
=》 (i + j)(i - j) % m = 0
由于m为质数,m的因子只有m和1
所以 (i + j) % m = 0 或 (i - j) % m = 0
①对于 (i - j) % m = 0
由
0 <= i <= m-1
0 <= j <= m-1
推出
-m <= 1 - m <= i - j <= m - 1 <= m
所以 i - j 不会是 -m 和 m
(i - j) % m = 0不可能成立
②对于 (i + j) % m = 0
由
0 <= i <= m-1
0 <= j <= m-1
推出
0 <= i + j <= 2 * (m - 1)
则只可能i + j = m的情况下 能推出(i + j) % m = 0
那么假设i和j在[0,(m-1)/2]之间的话,i和j不会等于m
由①和②可知,在[0, (m-1)/2],不存在两个探测次数i和j(i ≠ j),使得它们探测到的位置相同
因此,如果m为质数的话哈希表的负载因子小于(m-1)/2的时候(小于哈希表一半的长度),往哈希表中插入值不会发生碰撞
即如果选择表大小 m 为素数,那么二次探测至少会在重复前访问表的一半条目
这意味着只要负载因子小于 1/2且 m 是素数的时候,它就能在重复访问同一个位置前成功找到一个空槽位
3.混合运算中使用质数提高分布效果
在Java的String
类中使用的哈希码计算方法就用到了质数31
int hash = 0;
for (int i = 0; i < string.length(); i++) {
hash = 31 * hash + string.charAt(i);
}
在计算机中通过位移操作可以快速实现31倍的乘法:31 * x == (x << 5) - x,
jvm会帮你完成这件事,这样既提高了效率,又保证了良好的分布效果
4.使用增容质数表进行哈希表扩容
当哈希表进行扩容的时候,需要为新的哈希表指定大小,算法科学家列出了一个增容质数表,每一项都是前一项的差不多的两倍,新的哈希表大小就可以根据这个进行增长
例子:C++的SGI STL的质数增容表
static const unsigned long __stl_prime_list[__stl_num_primes] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};