散列表之散列函数
我们在之前的文章《散列表之链接法》中已经提到过,散列函数是散列表的一个难点,一个好的散列可以很大程度上提升散列表的查找和删除操作的速度,而一个设计差劲的散列表的,查找和删除操作的运行时间将和链式链表一样,将达到 O(n) 。所以设计一个表现良好的散列函数尤为重要!
什么是好的散列函数
一个好的散列函数应该满足简单均匀散列
的假设:
每一个关键字都被等可能的散列到m个槽中的任何一个,并与其它关键字散列到那个槽无关。
在实际应用中,我们可以通过利用关键字有用的分布信息来设计一个表现良好的散列函数。假设现在我们需要散列一些英文字符串,其中有一些比较相近的单词,比如the
,then
,he
等等,好的散列函数是将这些键值比较相近的字符串映射到相同槽的可能性最小化。
将关键字转化为自然数
什么?关键字除了自然数难道还有其他么?是的,关键字除了自然数还有许多其他的种类,比如字符串,浮点数,一个类对象,一些可以将自身转化为自然数的类型。转化为自然数以后,进行一些计算映射工作就得到槽的位置了。
比如对于一个字符串来说,我们将每个字符的ASCII码相加就得到一个自然数。一个小数的话,可以进行小数的截断或者通过乘 10n 来将小数扩大为自然数,而对于一个类对象来说,可以自定义一个将类对象转为自然数的函数方法。
举个例子:
class Person
{
//menbers
int age;
string name;
//function 转为自然数
size_t toN() const
{
/** 将age和name进行一些计算以后返回一个自然数 */
}
}
散列函数的三种设计方法
接下来我们将介绍三种散列函数的设计方法,他们分别是:
- 除法散列法
- 乘法散列法
- 全域散列法
除法散列法
除法散列法的定义,假设有一元素
E
,其键值为
比如 m=3 , k=10 ,那么 h(k)=1
除法散列法注意事项:
在应用触发散列法时,要避免 m 的某些值,例如m 不应该为2的幂,因为如果 m=2p ,则 h(k) 就是 k 的最低p 位数字(因为我们除以 2p 就是将 k 右移p 位,那么取余就是 k 的最低p 位数字),除非已经知道各种最低的 p 位的排列形式是等可能的,否则在设计散列函数的时候,最好考虑关键字的所有位
乘法散列法
构造散列函数的乘法散列法包含了两个步骤。第一步,用关键字
全域散列法
假设现在你写了一款软件,打算卖给一家公司,可是另外一个人也写了一款可以实现和你相同功能的软件,也打算卖给这家公司。于是公司就叫你们在看的见对方全部源码的情况下为对方写测试用例,最后那个软件运行速度快就买那个。假设双方在代码里面都使用了散列表,并且都设计了对应的散列函数,那么你的竞争对手就会针对你的散列函数写一个将全部键值都映射到同一个槽的恶意测试用例,而你也会这样写一组针对对方散列函数的恶意测试用例。那你要怎么才能在这场竞争中胜出呢?
没错,那就是随机,你写了若干个性能优良的散列函数,然后再运行的时候随机在里面选出一个散列函数进行映射,这样你的竞争对手就无法写出针对你的散列函数的恶意测试用例了
在前面我们讲过的除法散列法
和乘法散列法
都是一个固定的散列函数,在全域散列法里面,散列函数都是随机的,不过这些随机的散列函数可不是任意设计的。
设 H 是一组有限散列函数集合,它将给定的关键字全域
U 映射到 {0,1,2,3,...,m−1} ,这样的一个函数称为全域的,如果对于每一对不同的关键字 k,l∈U ,满足 h(k)=h(l) 的散列函数 h∈H 的个数至多是 |H|/m ,也就是说,从 H 中选取一个散列函数h ,在 k≠l 的情况下, h(k)=h(l) 的概率不大于 1/m 。这也正好是从集合 0,1,2,3,...,m−1 中独立的随机选取 h(k) 和 h(l) 发生冲突的概率。
那要怎么设计一个全域散列函数类呢?
- 首先选取一个足够大的素数
p
,使得每一个可能的关键字落到
0 到 p−1 的范围内。设 Zp={0,1,2,...,p−1} , Z∗p={1,2,3,...,p−1} - 现在对于
a∈Z∗p
,
b∈Zp
,定义散列函数
hab
.利用一次线性变换,进行模
m
和模
p 的归约,有
hab(k)=((ak+b)modp))modm
于是构成了这样的散列函数簇:
Hpm={hab:a∈Z∗p,b∈Zp}
定理: Hpm 是全域的
证明:考虑 Zp 中两个不同的关键字 k 和l ,即 k≠l ,对于某一个给定的散列函数 hab ,设:
r=(ak+b)modps=(al+b)modp
上面两式相减:
r−s≡a(k−l)modp
因为 p 是素数,且a modp≠0 和 (k−l) modp≠0 的,所以 a(k−l)modp≠0 ,即 r≠s
当 r 和s 为随机选取不同的值时,不同的关键字 k 和l 发生冲突的概率为 r≡s(modp) 的概率,对于某个给定的 r 值,s 的可能取值就是余下的 p−1 种,其中满足 s≠r 且 s≡r(modm) 的 s 值的数目至多是:
⌈p/m⌉−1≤((p+m−1)/m)−1=(p−1)/m
所以有:
Prhab(k)=hab(l)≤1/m