一、哈希表
线性表和树表的查找都是通过比较关键字的方法,查找的效率取决于关键字的比较次数。有没有一种查找方法可以不进行关键字比较,直接找到目标?
散列表是根据关键字直接进行访问的数据结构。散列表通过散列函数将关键字映射到存储地址,建立了关键字和存储地址之间的一种直接映射关系。
散列函数,又称为哈希函数,是将关键字映射到存储地址的函数,记为hash(key)=Addr。
例如,关键字key=(17,24,48,25),散列函数H(key)=key%5,散列函数将关键字映射到存储地址下标,将关键字存储到散列表的对应位置。
理想情况下,散列表查找的时间复杂度为O(1)。但是,散列函数可能会把两个或两个以上的关键字映射到同一地址,发生“冲突”,发生冲突的不同关键字称为“同义词”。
例如,48和13通过散列函数H(key)=key%5计算的映射地址都是3,13和48为“同义词”。
因此,设计散列函数时需要遵循以下2个原则——
- 散列函数尽可能简单,能够快速计算关键字的散列地址。
- 散列函数映射的地址应均匀分布整个地址空间,避免聚集,以减少冲突。
二、散列函数
常见的散列函数——
1.直接定址法
直接取关键字的某个线性函数作为散列函数:
hash(key)=a×key+b
其中,a、b为常数。
适用场景:事先知道关键字,关键字集合不是很大且连续性较好。关键字如果不连续,则有大量空位,造成空间浪费。
场景:关键字分布有规律,比如学生成绩在0-100之间,且连续性比较好。
遇到具体问题时要分析数据的分布规律来决定采用什么散列函数
2.除留余数法
除留余数法是一种最简单和常用的构造散列函数的方法,并且不需要事先知道关键字的分布。假定散列表的表长为m,取一个不大于表长的最大素数p,则设计散列函数为:
hash(key)=key%p
为什么取一个不大于表长的最大素数p?
不大于表长很好理解,防止溢出;最大也好理解,p太小了关键字映射得比较密集,很容易冲突;素数是为了避免冲突,在实际应用中,访问往往具有某种周期性,若周期与p有公共的素因子,则冲突的概率将急剧上升,例如,手表中的齿轮,两个交合齿轮的齿数最好是互质的,否则出现齿轮磨损绞断的概率很大,因此发生冲突的概率随着p所含素因子的增多而迅速增大,素因子越多,冲突越多。
不知道表长,可以提前准备一些素数,如109、1009、10009对应表长110、1010、10010.
三、冲突处理方法
无论如何设计散列函数,都无法避免冲突问题。发生冲突时,需要进行冲突处理。冲突处理方法分为3种:开放地址法、链地址法、建立公共溢出区。
1.开放地址法
开放地址法是在线性存储空间商的解决方案,称为闭散列。发生冲突时,采用冲突处理方法在线性存储空间上探测其他位置(不会另外开辟空间)。
其中,hash(key)为原散列函数,为探测函数,
为增量序列,m为表长。
根据增量序列的不同,开放地址法又分为线性探测法(常用)、二次探测法、随机探测法(常用)、再散列法。
(1)线性探测法
线性探测法是最简单的开放地址法,线性探测的增量序列:=1,……,m-1。
例如,一组关键字(14,36,42,38,40,15,19,12,51,65,34,25),若表长为15,散列函数为hash(key)=key%13,采用线性探测法处理冲突,构造该散列表。
40%13=1,但地址1已有数据,新地址=(1+1)%15=2,查找时需要比较两次(在地址1比较一次,在地质2比较一次)
15%13=2,但地址2已有数据,向后移动,地址3也有数据,再向后移动,地址4为空,所以地址为4,查找时需要比较3次
如此重复操作,得到下表
线性探测法很简单,只要有空间,就一定能够探测到位置。但是,在处理冲突的过程中,会出现非同义词之间对同一个散列地址争夺的现象,称为“堆积”。堆积大大降低了查找效率。如40和15,原来40的地址是1,但地址1已有数据,所以向后移动,占了本来属于15的位置
查找成功的平均查找长度=所有关键字查找成功的比较次数乘以查找概率