散列的基本概念
散列(Hash)同顺序、链式和索引存储结构一样,是存储线性表的又一种方法。散列存储的基本思想是:以线性表中的每个元素的关键字key为自变量,通过一种函数H(key)计算出函数值,把这个函数值解释为一块连续存储空间的单元地址(即下标),将该元素存储到这个单元中。散列存储中使用的函数H(key)称为散列函数或哈希函数,它实现关键字到存储地址的映射(或称转换),H(key)的值称为散列地址或哈希地址;使用的数组空间是线性表进行散列存储的地址空间,所以被称之为散列表或哈希表。当在散列表上进行查找时,首先根据给定的关键字key,用与散列存储时使用的同一散列函数H(key)计算出散列地址,然后按此地址从散列表中取对应的元素。
例如,有个线性表A=(31,62,74,36,49,77),其中每个整数可以是元素本身,也可以仅是元素的关键字,为了散列存储该线性表,假设选取的散列函数为:
H(key)=key % m
即用元素的关键字key整除m,取其余数作为存储该元素的散列地址,m一般取小于或等于散列表长的最大素数,在这里取m=11,表长也为11,因此可得到每个元素的的散列地址:
H(31)=31 % 11=9; H(62)=62 % 11=7;
H(74)=74 % 11=8; H(36)=36 % 11=3;
H(49)=49 % 11=5; H(77)=77 % 11=0;
如果根据散列地址把上述元素存储到散列表HT[m]中,则存储结构为:
从散列表中查找元素与插入元素一样简单,如从HT中查找关键字为36的元素时,只要利用上述散列函数H(key)计算出key=36时的散列地址3,则从下标为3的单元中取出该元素即可。
在上面的散列表上插入时根据元素的关键字计算出的散列地址,其对应的存储单元都是空闲的,没有出现该单元已被其它元素占的情况。在实际应用中这种理想的情况是很少见的,例如,要在上面的散列表中插入一个关键字为19的元素时,计算出其散列地址为8,而8号单元已被关键字为74的元素所占用,通常我们把这种现象称为冲突。具相同散列地址的关键字称为同义词。因此,在设计散列函数时,要尽量减少或没有冲突存在,但少量的冲突往往是不可避免的。这样就存在如何解决冲突的问题。冲突的频度除了与散列函数H相关外,还与散列表的填满程度相关。因此,如何尽量避免冲突和冲突发生后如何解决冲突就成了散列存储的两个关键问题。
构造散列函数的目标是使散列地址尽可能均匀分布在散列空间上,同时使计算尽可能简单。构造散列函数的方法很多,常用的构造散列函数的方法有如下几种。
1.直接地址法
直接地址法是以关键字key本身或关键字加上某个常量C作为散列地址的方法。对应的散列函数H(key)为:
H(key)=key+c
在使用时,为了使散列地址与存储空间吻合,可以调整c。这种方法计算简单,并且没有冲突。它适合于关键字的分布基本连续的情况,若关键字分布不连续,空号较多,将会造成较的空间浪费。
2. 数字分析法
数字分析法是假设有一组关键字,每个关键字由n位数字组成,如k1,k2…kn。数字选择法是从中提取数字分布比较均匀的若干位作为散列地址。
例如,有一组有6位数字组成的关键字,如下表左边一列表示:
关键字 散列地址(0..99)
分析这一组关键字会发现,第1、3、5和6位数字分布不均匀,第1位数字全是9或8,第3位基本上都是2,第5、6两位上也都基本上是5和6,故这4位不可取。而第2、4两位数字分布比较均匀,因此可取关键字中第2、4两位的组合作为散列地址,如上表的右边一列所示。
3. 除余数法
除余数法是选择一个适当的p(p≤散列表长m)去除关键字k,所得余数作为散列地址的方法。对应的散列函数H(k)为:
H(k)=k % p
其中p最好选取小于或等于表长m的最大素数。如表长为20,那么p选19,若表长为25,则p可选23,…。 表长m与模p的关系可按下表对应:
m=8,16,32,64,128,256,512,1024,…
p=7,13,31,61,127,251,503,1019,…
这是一种最简单,也是最常用的一种散列函数构造方法。在上一节中我们已经使用过。
4. 平方取中法
平方取中法是取关键字平方的中间几位作为散列地址的方法,因为一个乘积的中间几位和乘数的每一位都相关,故由此产生的散列地址较为均匀,具体取多少位视实际情况而定。例如有一组关键字集合(0100,0110,0111,1001,1010,1110),平方之后得到新的数据集合(0010000,0012100,0012321,1002001,1020100,123210),那么,若表长为1000,则可取其中第3、4和5位作为对应的散列地址为 (100,121,123,020,201,321)。
(5) 折叠法
折叠法是首先把关键字分割成位数相同的几段(最后一段的位数可少一些),段的位数取决于散列地址的位数,由实际情况而定,然后将它们的叠加和(舍去最高进位)作为散列地址的方法。
折叠法又分移位叠加和边界叠加。移位叠加是将各段的最低位对齐,然后相加;边界叠加则是将两个相邻的段沿边界来回折叠,然后对齐相加。
例如,关键字k=98123658,散列地址为3位,则将关键字从左到右每三位一段进行划分,得到的三个段为981、236和58,叠加后值为1275,取低3位275作为关键字98123658的元素的散列地址; 如若用边界叠加,即为981、632和58叠加后其值为1671,取低3位得671作为散列地址。
散列法构造表可通过散列函数的选取来减少冲突,但冲突一般不可避免,为此,需要有解决冲突的方法,常用的解决冲突的方法有两大类,即开放定址法和链地址法。
1.开放定址法
开放定址法又分为线性探插法、二次探查法和双重散列法。开放定址法解决冲突的基本思想是:使用某种方法在散列表中形成一个探查序列,沿着次序列逐个单元进行查找,直到找到一个空闲的单元时将新结点存入其中。假设散列表空间为T[0..m-1],散列函数H(key),开放定址法的一般形式为:hi=(H(key)+di) % m 0≤i≤m-1
其中di为增量序列,m为散列表长。h0 =H(key)为初始探查地址(假d0=0),后续的探查地址依次是h1,h2,…,hm-1 。
(1) 线性探查法
线性探查法的基本思想是:将散列表T[0..m-1]看成一个循环向量,若初始探查的地址为d(即H(key)=d),那么,后续探查地址的序列为:d+1,d+2,…,m-1,0,1,…,d-1。也就是说,探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,T[m-1],此后又循环到T[0],T[1],…,T[d-1]。分两种情况分析,一种运算是插入:若当前探查单元为空,则将关键字key写入空单元,若不空则继续后序地址探查,直到遇到空单元插入关键字,若探查到T[d-1]时仍未发现空单元,则插入失败(表满);另一种运算是查找,若当前探查单元关键字值等于key,则表示查找成功,若不等于,则继续后序地址探查,若遇到单元关键字值等于key时,查找成功,若探查T[d-1]时仍未发现关键字值等于key,则查找失败。
(2) 二次探查法
二次探查法的探查序列是: hi=(H(key)+i2) % m 0≤i≤m-1
即探查序列为:d=H(key),d+12,d+22,…,等。也就是说,探查从地址d开始,先探查T[d],然后在依次探查T[d+12],T[d+22],…。
(3) 双重散列法
双重散列法是几种方法中最好的方法,它的探查序列为:
hi=(H(key)+i*H1(key)) % m 0≤i≤m-1
即探查序列为:d=H(key),(d+1*H1(key)) % m,(d+2*H1(key)) % m,…,等。
【例9.4】设散列函数为h(key)=key % 11;散列地址表空间为0~10,对关键字序列{27,13,55,32,18,49,24,38,43},利用线性探测法解决冲突,构造散列表。
解:首先根据散列函数计算散列地址:
h(27)=5; h(13)=2;
h(55)=0; h(32)=10;
h(18)=7; h(49)=5;
h(24)=2; h(38)=5;
h(43)=10;
(散列表各元素查找比较次数标注在结点的上方或下方)
根据散列函数计算得到的散列地址可知,关键字27、13、55、32、18插入的地址均为开放地址,将它们直接插入到T[5],T[2],T[0],T[10],T[7]中。当插入关键字49时,散列地址5已被同义词27占用,因此探查h1=(5+1) % 11=6,此地址为开放地址,因此可将49插入到T[6]中;当插入关键字24时,其散列地址2已被同义词13占用,故探测地址h1=(2+1) % 11=3, 此地址为开放地址,因此可将24插入到T[3]中;当关键字38插入时,散列地址5已被同义词27占用,探查h1=(5+1) % 11=6,也被同义词49占用,再探查h2=(5+2)=7,地址7已被非同义词占用,因此需要再探查h3=(5+3) % 11=8,此地址为开放地址,因此可将38插入到T[8]中;当插入关键字43时,计算得到散列地址10已被关键字32占用,需要探查h1=(10+1) % 11=0,此地址已被占用,探查h2=(10+1) % 11=1为开放地址,因此可将43插入到T[1]中;由此构造的散列表如图9.13所示。
2.拉链法(链地址法)
当存储结构是链表时,多采用拉链法,用拉链法处理冲突的办法是:把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。有m个散列地址就有m个链表,同时用指针数组T[0..m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以T[i]为指针的单链表中。T中各分量的初值应为空指针。
例如,按上面例9.4所给的关键字序列,用拉链法构造散列表如图9.14所示。
用拉链法处理冲突,虽然比开放定址法多占用一些存储空间用做链接指针,但它可以减少在插入和查找过程中同关键字平均比较次数(平均查找长度),这是因为,在拉链法中待比较的结点都是同义词结点,而在开放定址法中,待比较的结点不仅包含有同义词结点,而且包含有非同义词结点,往往非同义词结点比同义词结点还要多。
如前面介绍的例9.4中,用线性探测法构造散列表的过程,我们知道,对前5个关键字的查找,每一个仅需要比较一次,对关键字49和24的查找,则需要比较2次,对关键字38的查找则需要比较4次,而对43的查找则需要比较3次。因此,对用线性探测法构造的散列表的平均查找长度为:
ASL=(1×5+2×2+3×1+4×1)/9 ≈1.78
而用拉链法构造的散列表上查找成功的平均查找长度为:
ASL=(1×5+2×3+3×1)/9≈1.55
显然,开放定址法处理冲突的的平均查找长度要高于拉链法处理冲突的平均查找长度。但它们都比前面介绍的其它查找方法的平均查找长度要短。