哈希表(查找+分治)

一、哈希表

线性表和树表的查找都是通过比较关键字的方法,查找的效率取决于关键字的比较次数。有没有一种查找方法可以不进行关键字比较,直接找到目标?

散列表是根据关键字直接进行访问的数据结构。散列表通过散列函数关键字映射存储地址,建立了关键字和存储地址之间的一种直接映射关系。

散列函数,又称为哈希函数,是将关键字映射到存储地址的函数,记为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)为原散列函数,{hash}'(key)为探测函数,d_{i}为增量序列,m为表长。

根据增量序列的不同,开放地址法又分为线性探测法(常用)、二次探测法、随机探测法(常用)、再散列法

(1)线性探测法

线性探测法是最简单的开放地址法,线性探测的增量序列:d_{i}=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的位置

查找成功的平均查找长度=所有关键字查找成功的比较次数c_{i}乘以查找概率

猜你喜欢

转载自blog.csdn.net/qq_55078860/article/details/140963185
今日推荐