散列表是根据关键字直接进行访问的数据结构。散列表通过散列函数将关键字映射到存储地址,建立了关键字和存储地址之间的一种直接映射关系。
散列函数(Hash function),又称为哈希函数,是将关键字映射到存储地址的函数,相当于一种映射规则。
方便理解,下面举例两个常见的散列函数
直接定址法:直接取关键字的某个线性函数作为散列函数,散列函数形式如下:
hash(key)=a*key+b
例如,学生的学号{601001, 601002, 601005, …, 601045},那么可以设计散列函数为:H(key)=key-601000这样可以将学生的学号直接映射到存储地址下标,符合简单均匀的原则
除留余数法:除留余数法是一种最简单、最常用的构造散列函数的方法,并且不需要求事先知道关键字的分布。假定散列表的表长为m,取一个不大于表长的最大素数p,则设计散列函数为:hash(key)=key%p
无论如何设计散列函数,都无法避免冲突问题。如果发生冲突,就需要进行冲突处理。冲突处理方法分为3种:开发地址法、链地址法、建立公共溢出区。
开放地址法是在线性存储空间上的解决方案,也称为闭散列
当发生冲突时,采用冲突处理方法在线性存储空间上探测其他的位置。
hash′(key)=(hash(key)+di)%m
其中,hash(key)为原散列函数,hash′(key)为探测函数,di为增量序列,m为表长。根据增量序列的不同,开放地址法又分为线性探测法、二次探测法等。
线性探测法是最简单的开发地址法,线性探测的增量序列如下:
di=1, …, m-1
例如,一组关键字(14, 36, 42, 38, 40, 15, 19, 12, 51, 65, 34, 25),若表长为15,散列函数为hash(key)=key,采用线性探测法处理冲突,构造该散列表
图解:
按照关键字顺序,根据散列函数计算散列地址,如果该地址空间为空,则直接放入,如果该地址空间已存有数据,则采用线性探测法处理冲突
hash(14)=14=1,将14放入1号空间(下标为1)
hash(36)=36=10,将36放入10号空间
hash(42)=42=3,将42放入3号空间
hash(38)=38=12,将38放入12号空间
hash(40)=40=1,1号空间已存储数据,采用线性探测处理冲突:
hash′(40)=(hash(40)+di)%m, di=1, …,m-1
d1=1:hash′(40)=(1+1)=2,2号空间为空,将40放入2号空间。即hash(40)=40=1→2
hash(15)=15=2,2号空间已存储数据,发生冲突,采用线性探测处理冲突:
hash′(15)=(hash(15)+di)%m, di=1, …,m-1
d1=1:hash′(15)=(2+1)=3,3号空间已存数据,继续线性探测
d2=2:hash′(15)=(2+2)=4,4号空间为空,将15放入4号空间。即hash(15)=15=2→3→4
hash(19)=19=6,将19放入6号空间
hash(12)=12=12,12号空间已存储数据,采用线性探测处理冲突:
hash′(12)=(hash(12)+di)%m, di=1, …,m-1
d1=1:hash′(12)=(12+1)=13,13号空间为空,将12放入13号空间。即hash(12)=12=12→13
hash(51)=51=12,12号空间已存储数据,采用线性探测处理冲突:
hash′(51)=(hash(51)+di)%m, di=1, …,m-1
d1=1:hash′(51)=(12+1)=13,13号空间已存数据,继续线性探测
d2=2:hash′(51)=(12+2)=14,14号空间为空,将51放入14号空间。即hash(51)=51=12→13→14
直接来的步骤相似不再赘述,最后直接可以得到:
从图中可以看出,1次比较成功的有7个,2次比较成功的有2个,3次比较成功的有2个,9次比较成功的有1个,乘以查找概率求和,因为查找概率均为1/12,也可以理解为比较次数求和后除以关键字个数12。
其查找成功的平均查找长度如下:
ASLsucc=(1×7+2×2+3×2+9)/12=4/3
二次探测法采用前后跳跃式探测的方法,发生冲突时,向后1位探测,向前1位探测,向后22位探测,向前22位探测……跳跃式探测,避免堆积
二次探测的增量序列为如下:
di=12,-12,22,-22, …,k2,-k2(k≤m/2)
例如,一组关键字(14,36,42,38,40,15,19,12,51,65,34,25),若表长为15,散列函数为hash(key)=key,采用二次探测法处理冲突,构造该散列表。
图解
按照关键字顺序,根据散列函数计算散列地址,如果该地址空间为空,则直接放入,如果该地址空间已存有数据,则采用线性探测法处理冲突。
hash(14)=14=1,将14放入1号空间(下标为1)。hash(36)=36=10,将36放入10号空间。hash(42)=42=3,将42放入3号空间。hash(38)=38=12,将38放入12号空间hash(40)=40=1,1号空间已存储数据,采用二次探测处理冲突:
hash′(40)=(hash(40)+di)%m, di=12,-12,22,-22, …,k2,-k2(k≤m/2)
d1=1^2:hash′(40)=(1+12)=2,2号空间为空,将40放入2号空间。即hash(40)=40=1→2
hash(15)=15=2,2号空间已存储数据,发生冲突,采用二次探测处理冲突:
d1=12:hash′(15)=(2+12)=3,3号空间已存数据,继续二次探测。d2=-12:hash′(15)=(2-12)=1,1号空间已存数据,继续二次探测。d3=22:hash′(15)=(2+22)=6,6号空间为空,将15放入6号空间。即hash(15)=15=2→3→1→6简单的来讲,2号空间非空,往后+12(1)到3号空间,3号空间非空,往前-12(1)到1号空间,1号空间非空,往后+2^2(4)到6号空间,为空,塞进去,就像个弹珠一样前后前来前去,直到找到“安身之所”
hash(51)=51=12,12号空间已存储数据,采用二次探测处理冲突:
d1=12:hash′(51)=(12+1^2)=13,13号空间已存数据,继续二次探测
d2=-12:hash′(51)=(12-1^2)=11,11号空间为空,将51放入11号空间。即hash(51)=51=12→13→11
hash(65)=65=0,将65放入0号空间
hash(34)=34=8,将34放入8号空间
hash(25)=25=12,12号空间已存储数据,采用二探测处理冲突。注意:二次探测过程中如果二次探测地址为负值,则加上表长即可。
d1=12:hash′(25)=(12+1^2)=13,已存数据,继续二次探测
d2=-12:hash′(25)=(12-1^2)=11,已存数据,继续二次探测
d3=22:hash′(25)=(12+2^2)=1,已存数据,继续二次探测
d4=-22:hash′(25)=(12-2^2)=8,已存数据,继续二次探测
d5=32:hash′(25)=(12+3^2)=6,已存数据,继续二次探测
d6=-42:hash′(25)=(12-3^2)=3,已存数据,继续二次探测
d7=42:hash′(25)=(12+4^2)=13,已存数据,继续二次探测
d8=-42:hash′(25)=(12-4^2)=-4, -4+15=11,已存数据,继续二次探测
d9=52:hash′(25)=(12+5^2)=7,已存数据,继续二次探测
d10=-52:hash′(25)=(12-5^2)=-13, -13+15=2,已存数据,继续二次探测
d11=62:hash′(25)=(12+6^2)=3,已存数据,继续二次探测
d12=-62:hash′(25)=(12-6^2)=-9, -9+15=6,已存数据,继续二次探测
d13=72:hash′(25)=(12+7^2)=1,已存数据,继续二次探测
d14=-72:hash′(25)=(12-7^2)=-7, -7+15=8,已存数据,继续二次探测
即12→13→11→1→8→6→3→13→11→7→2→3→6→1→8,没有找到位置,存储失败
所以由于二次探测法是跳跃式探测,效率较高,但是会出现明明有空间却探测不到的情况,因而存储失败,而线性探测只要有空间就一定能够探测成功
链地址法又称为拉链法。如果不同关键字通过散列函数映射到同一地址,这些关键字为同义词,将所有的同义词存储在一个线性链表中。查找、插入、删除操作主要在这个链表中进行,拉链法适用于经常进行插入、删除的情况。
例如,一组关键字(14, 36, 42, 38, 40, 15, 19, 12, 51, 65, 34, 25),若表长为15,散列函数为hash(key)=key,采用链地址法处理冲突,构造该散列表。
图解
按照关键字顺序,根据散列函数计算散列地址,如果该地址空间为空,则直接放入;如果该地址空间已存有数据,则采用链地址法处理冲突
hash(14)=14=1,放入1号空间后面的单链表中
hash(36)=36=10,放入10号空间后面的单链表中
hash(42)=42=3,放入3号空间后面的单链表中
hash(38)=38=12,放入12号空间后面的单链表中
hash(40)=40=1,放入1号空间后面的单链表中
hash(15)=15=2,放入2号空间后面的单链表中
hash(19)=19=6,放入6号空间后面的单链表中
hash(12)=12=12,放入12号空间后面的单链表中
hash(51)=51=12,放入12号空间后面的单链表中…
最后可以得到:
从图中可以看出:1次比较成功的有8个,2次比较成功的有2个,3次比较成功的有1个,4次比较成功的有1个。其查找成功的平均查找长度为:
ASLsucc=(1×8+2×2+3+4)/12=19/12
本题中散列函数为hash(key)=key,计算得到的散列地址为0, 1, …, 12,一共有13种情况
假设查找失败的概率均等(13种失败情况,每种情况的概率为1/13),查找失败的平均查找长度等于所有关键字的查找失败的比较次数乘以概率之和
当hash(key)=0时,如果该空间为空,则比较1次即可确定查找失败;如果该空间非空,则在其后面的单链表中查找,直到空时,确定查找失败。如果单链表中有两个节点,则需要比较3次才能确定查找失败。类似地,hash(key)=1, …,12时也如此计算,如图:
其中,5个空,比较1次失败;6个含有1个节点,比较2次失败;1个含有2个节点,比较3次失败;1个含有4个节点,比较5次失败。其查找失败的平均查找长度为:
ASLunsucc=(1×5+2×6+3+5)/13=25/13
(以上图文整理于《趣学数据结构》)