哈希表存储的基本思路是:设要存储的元素个数为n,设置一个长度为m (m≥n)的连续内存单元,以每个元素的关键字ki(0≤i≤n—1)为自变量,通过一个称为哈希函数的函数h(ki),把ki映射为内存单元的地址(或称下标)h(ki),并把该元素存储在这个内存单元中。h(ki)也称为哈希地址(又称散列地址)。把如此构造的线性表存储结构称为哈希表。
但是存在这样的问题,对于两个不同元素的关键字ki和kj(i≠j),有h(ki)=h(kj)。这种现象叫作哈希冲突。通常把这种具有不同关键字而具有相同哈希地址的元素称作“同义词”,因此这种冲突也称为同义词冲突。在哈希表存储结构中,同义词冲突是很难避免的,除非关键字的变化区间小于等于哈希地址的变化区间,而这种情况当关键字取值不连续时是非常浪费存储空间的。通常的实际情况是关键字的取值区间远大于哈希地址的变化区间。
构造哈希函数的目标是使得到n个元素的哈希地址尽可能均匀地分布在m个连续内存单元地址上,同时使计算过程尽可能简单以达到尽可能高的时间效率。根据关键字的结构和分布的不同,有多种构造哈希函数的方法。这里主要讨论几种常用的整数类型关键字的哈希函数构造方法。1.直接定址法
直接定址法是以关键字k本身或关键字加上某个常量c作为哈希地址的方法。直接定址法的哈希函数h(k)为:h(k)=k+c
这种哈希函数计算简单,并且不可能有冲突发生,第1章中如图1.10所示的哈希存储结构就是采用的这种方法。当关键字的分布基本连续时,可用直接定址法的哈希函数;否则,若关键字分布不连续将造成内存单元的大量浪费。2.除留余数法
除留余数法是用关键字k除以某个不大于哈希表长度m的整数p所得的余数作为哈希地址的方法。除留余数法的哈希函数h(k)为:h(k)=kmodp (mod为求余运算,p≤m)
除留余数法计算比较简单,适用范围广,是最经常使用的一种哈希函数。这种方法的关键是选好p,使得元素集合中的每一个关键字通过该函数转换后映射到哈希表范围内的任意地址上的概率相等,从而尽可能减少发生冲突的可能性。例如,p取奇数就比p取偶数好。理论研究表明,p取不大于m的素数时效果最好。
解决哈希冲突的方法有许多,主要分为开放定址法和拉链法两大类。其基本思路是当发生哈希冲突时,即当ki≠kj(i≠j),而h(ki)=h(kj)时,通过哈希冲突函数(设为hl(k),这里l=1,2,…,m—1)产生一个新的哈希地址,使hl(ki)≠hl(kj)。哈希冲突函数产生的哈希地址仍可能有哈希冲突问题,此时再用新的哈希冲突函数得到新的哈希地址,一直到不存在哈希冲突为止,因此有l=1,2,…,m—1。这样就把要存储的n个元素,通过哈希函数映射得到的哈希地址(当哈希冲突时通过哈希冲突函数映射得到的哈希地址)存储到了m个连续内存单元中,从而完成了哈希表的建立。说明:对于预先知道且规模不大的关键字集,通常可以找到不发生冲突的哈希函数,从而避免出现冲突,使查找时间复杂度为O(1),提高了查找效率。因此对频繁进行查找的关键字集,应尽力设计一个完美的哈希函数。
在哈希表中,虽然冲突很难避免,但发生冲突的可能性却有大有小。这主要与以下三个因素有关。
(1)与装填因子α有关。装填因子是指哈希表中已存入的元素个数n与哈希地址空间大小m的比值,即α=n/m,α越小,冲突的可能性就越小;α越大(最大可取1),冲突的可能性就越大。这很容易理解,因为α越小,哈希表中空闲单元的比例就越大,所以待插入元素同已插入的元素发生冲突的可能性就越小;反之,α越大,哈希表中空闲单元的比例就越小,所以待插入元素同已插入的元素冲突的可能性就越大。另一方面,α越小,存储空间的利用率就越低;反之,存储空间的利用率也就越高。为了既兼顾减少冲突的发生,又兼顾提高存储空间的利用率这两个方面,通常使最终的α控制在0.6~0.9的范围内。
(2)与所采用的哈希函数有关。若哈希函数选择得当,就可使哈希地址尽可能均匀地分布在哈希地址空间上,从而减少冲突的发生;否则,若哈希函数选择不当,就可能使哈希地址集中于某些区域,从而加大冲突的发生。
(3)与解决冲突的哈希冲突函数有关。哈希冲突函数选择的好坏也将减少或增加发生冲突的可能性。
开放定址法就是一旦发生了冲突,就去寻找下一个空的哈希地址,只要哈希表足够大,空的哈希地址总能找到,并将有冲突的元素存入该空的哈希地址处。
所以,开放定址法以发生冲突的哈希地址为自变量,通过某种哈希冲突函数得到一个新的空闲的哈希地址。在开放定址法中,哈希表中的空闲单元(假设其下标或地址为d)不仅允许哈希地址为d的同义词关键字使用,而且也允许发生冲突的其他关键字使用。开放定址法的名称就是来自此方法的哈希表空闲单元既向同义词关键字开放,也向发生冲突的非同义词关键字开放。至于哈希表的一个地址中存放的是同义词关键字还是非同义词关键字,要看谁先占用它,这和构造哈希表的元素排列次序有关。
在开放定址法中,以发生冲突的哈希地址为自变量。通过某种哈希冲突函数得到一个新的空闲的哈希地址的方法有很多种,下面介绍几种常用的方法。
拉链法也称链地址法,是将有冲突的元素用单链表链接起来。在这种方法中,哈希表每个单元中存放的不再是元素本身,而是相应同义词单链表的头指针。由于单链表中可插入任意多个结点,所以此时装填因子α根据同义词的多少既可以设定为大于1,也可以设定为小于或等于1,通常取α=1。
与开放定址法相比,拉链法有如下几个优点。
(1)拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短。
(2)由于拉链法中各链表上的元素空间是动态申请的,故它更适合于造表前无法确定表长的情况。
(3)开放定址法为减少冲突要求装填因子α较小,故当数据规模较大时会浪费很多空间,而拉链法中可取α≥1,且元素较多时,拉链法中增加的指针域可忽略不计,因此节省空间。
(4)在用拉链法构造的哈希表中,删除元素的操作易于实现,只要简单地删去链表上相应的元素即可。而对开放地址法构造的哈希表,删除元素不能简单地将被删元素的空间置为空,否则将截断在它之后填入哈希表的同义词元素的查找路径,这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放地址法处理冲突的哈希表上执行删除操作,只能在被删元素上做删除标记,而不能真正删除元素。
拉链法也有缺点,在相同哈希地址的元素构成的单链表中,链指针需要额外的空间,故当元素个数较少时,开放定址法较为节省空间,而若将节省的指针空间用来扩大哈希表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高了平均查找速度。另外,拉链法也带来了查找时需要遍历同义词单链表的性能损耗。