数据结构(四十)散列表查找(Hash Table)

  一、散列表查找的基础知识

  1.散列表查找的定义

  散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。查找时,根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定存在在f(key)的位置上。

  把对应关系f称为散列函数,又称为哈希(Hash)函数,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash Table)。关键字对应的记录存储位置称为散列地址。

  2.散列表查找步骤

  散列技术既是一种存储方法,也是一种查找方法。散列过程的步骤分为两步:

  • 在存储时,通过散列函数计算记录的散列地址,并按次散列地址存储该记录。
  • 当查找记录时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。

  3.散列表查找的适合场景

  散列技术与线性表、树、图结构不同的是,散列技术的记录之间不存在什么逻辑关系,它只与关键字有关联。因此,散列主要是面向查找的存储结构。

  散列技术最适合的求解问题是查找与给定值相等的记录。不适合同样的关键字对应很多记录或者范围查找。

  对于两个不同的关键字key1≠key2,但是却有f(key1)=f(key2),这种现象称为冲突,并把key1和key2称为这个散列函数的同义词。

  二、散列函数的构造方法

  散列函数设计的原则:1.计算简单、  2.散列地址分布均匀

  对于关键字是中文字符或者是因为字符或者是各种符号,都可以转化为某种数字来对待,例如ASCII码或者Unicode码等。

  实际应用中,根据不同的情况采用不同的散列函数,给出如下考虑因素:计算散列地址所需的时间;关键字的长度;散列表的大小;关键字的分布情况;记录查找的频率。

  1.直接定址法 :f(key) = a X key + b(a,b为常数)

  

  这样的散列函数优点就是简单、均匀也不会产生冲突,但问题是需要事先通知关键字的分布情况,适合查找表较小且连续的情况。

  2.数字分析法

  

  抽取方法是使用关键字的一部分来计算散列存储位置的方法,这在散列函数中是常常用到的手段。数字分析法通常适合处理关键字位数比较大的情况,如果实现知道关键字的分布且关键字的若干位分布较均匀,就可以考虑使用这个方法。

  3.平方取中法

  假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用作散列地址。平方取中法比较适合于不知道关键字的分布,而位数又不是很大的情况。

  4.折叠法

  折叠法是将关键字从左到右分割成位数相等的几部分,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

  例如关键字是9876543210,散列表表长为三位,分成四组叠加求和987+654+321+0=1962,再求后三位得到散列地址为962。

  折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。

  5.除留余数法(最常用的散列函数)

  对于散列表长为m的散列函数公式为:f(key) = key mod p (p ≤m)。关键在于p值的选取。

  p=11时只有12和144冲突。

  经验就是,若散列表表长为m,通常p为小于等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

  6.随机数法

  选择一个随机数,去关键字的随机函数值为它的散列地址。即f(key)=random(key)。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

  三、处理散列冲突的方法

  设计得再好的散列函数也不可能完全避免冲突。

  1.开放定址法:fi(key) = (f(key) + di) MOD m(di = 1,2,3...,m-1)

  开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

  假设关键字集合为{12,67,56,16,25,37,22,29,15,47,48,34},表长为12,f(key) = key MOD 12

  则f(12) = 0,f(67) = 7,f(56) = 8,f(16) = 4,f(25) = 1,而f(37) = 1,则f(37) = (f(37) + 1) MOD 12 = 2,继续f(22) = 10,f(29) = 5,f(15) = 3,f(47) = 11

  而f(48) = 0,冲突,f(48) = (f(48) + 1) MOD 12 = 1,也冲突,f(48) = (f(48) + 1) MOD 12 = 2,还是冲突,一直到f(48) = (f(48) + 6) MOD 12 = 6时才不冲突。

  把这种解决冲突的开放定址法称为线性探测法。

  例如48和37这种本来都不是同义词却需要争夺一个地址的情况,称为堆积。堆积使得需要不断处理冲突,无论是存入还是查找效率都会大大降低。

  

  当key=34时,f(key)=10,但是22后面没有空位置了,反而它的前面有一个空位置,尽管可以不断地求余数后得到结果,但效率很差。可以改进di=1²,-1²,2²,-2²,...,q²,-q²(q≤m/2),这样就等于是可以双向寻找到可能的空位置。

  增加平方运算的目的是为了不让关键字都聚集在某一块区域,称这种方法为二次探测法。fi(key) = (f(key) + di) MOD m ( di=1²,-1²,2²,-2²,...,q²,-q²(q≤m/2))

  还可以对于位移量di采用随机函数计算得到,称之为随机探测法。即设置随机种子相同,每次调用随机函数可以生成不会重复的数列,在查找时,用同样的随机种子,它每次得到的数列是相同的,相同的di可以得到相同的散列地址。fi(key) = (f(key) + di) MOD m(di是一个随机数列)

  2.再散列函数法

  再散列函数法就是事先准备多个散列函数fi(key) = RHi(key) (i=1,2,...,k),每当发生散列地址冲突时,就换一个散列函数计算,这种方法能够使得关键字不产生聚集,也相应增加了计算的时间。  

  3.链地址法

  将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表的头指针,无论有多少个冲突,都只是在当前位置给单链表增加结点而已。

  例如:0下标→48→12,1下标→37→25...

  链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保障。当然,也带来了查找时需要遍历单链表的性能损耗。

  4.公共溢出区法

  公共溢出区法就是将所有与之间的关键字位置有冲突的关键字{37,48,34}存入一个公共的溢出区表中。

  在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行对比,如果相等,则查找成功;如果不相等,则到溢出表中进行顺序查找。

  如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

  四、散列表查找的实现

  五、散列表查找的性能分析+

猜你喜欢

转载自www.cnblogs.com/BigJunOba/p/9291627.html