8.4 哈希表及其查找

南山隐士 2022年07月31日 37 0

首先理解哈希表是一个“数据元素”与其“存储位置”之间的对应表,通常用哈希函数实现。要求理解哈希表的基本概念,掌握处理冲突的方法—开放地址法。

线性表和树表的查找算法都是基于查找值与查找表中记录的关键字值的比较,也就是说这些方法都是建立在“比较”基础上的,算法的查找效率取决于查找过程中所进行的“比较”次数。能否仅通过对待查记录的关键字值(查找值)进行相应的计算,就能找到要查找的记录?很显然,这就需要在待查记录的关键字值与该记录的存储位置之间建立确定的对应关系。这就是本节要介绍的查找方法——哈希(Hash)表查找。

1.哈希表的基本概念

(1)哈希函数

哈希函数是记录的关键字值与该记录的存储地址之间所构造的对应关系。也就是说,给定一个关键字值,通过这一关系就能在存储结构中得到唯一的地址,把给定的关键字的记录存储到该地址的存储单元中,在查找时通过该地址值访问到相应记录。

(2)哈希表

是用来存放查找表中记录序列的表,每一个记录的存储位置是以该记录的关键字为自变量,由相应哈希函数计算所得到的函数值。

为保证哈希表查找能正确操作,必须使记录的存放规则和查找规则一致,也即要使用统一的哈希函数。存储时,以记录的关键字为自变量,通过哈希函数得到存储该关键字记录的地址,并把记录存储到相应地址的存储单元中。在查找时,同样以待查找记录的关键字为自变量,通过存储时所用的哈希函数计算得到存储地址,并通过该地址访问相应记录。

例如,将查找表{8,10,14,21}存储到编号为0到4的表长为5的哈希表中,定义计算哈希地址的哈希函数为H(K)=K mod 5,K是关键字值, 哈希函数的采用除以5取余的方法。哈希表结构如图8-9所示。


图8-9 哈希表

(3)冲突>

通过本例我们可以看出,由计算所得的哈希地址可以将各数据元素存储到哈希表的对应位置,之后我们再进行查找时,利用相同的函数重新计算便可以直接到该位置去找到相应元素。但是,如果我们在上述关键字序列中添加一个关键字28,会出现什么情况呢?结果会发现,H(28)=H(8)。如果把28存储到表中,必然会将原来的数据元素冲掉。我们称这种现象为冲突(Collision),换句话说就是不同的关键字值具有相同的哈希地址。这种具有相同函数值的关键字对该哈希函数来说被称为是同义词(Synonym)。

显然,如何避免冲突取决于哈希函数的构造,一个好的哈希函数,它能使哈希地址均匀地分布在整个哈希表的地址区间中,以尽量避免或减少冲突的发生。8.4.2中我们将具体介绍几种构造哈希函数的方法。一般情况下,冲突是很难避免的,我们只可能尽量去减小冲突。所以还要有研究和设计处理冲突的方法。

2.哈希函数的构造方法

(1)直接定址法>

当关键字是整数时,取关键字本身或者它的某个线性函数作为它的哈希地址。即:

H(K)=K 或者H(K)= aK+b(其中a,b为常数)

这种方法简单易懂,关键字不相同时不会产生冲突。但是,如果关键字分布不是连续的,则该方法所产生的哈希表可能会造成空间的大量浪费,因此,这种方法在实际中很少应用。

(2)平方取中法>

该方法是一种比较好的构造哈希函数的方法。先计算出关键字K的平方值,再取其平方值的中间若干位作为哈希地址。即:

H(K)=“K的平方值的中间几位”

关键字平方后使得它的中间几位和组成关键字的各位均有关系,从而使哈希地址的分布较为均匀,减少了发生冲突的可能。当然,所取的位数取决于哈希表的长度。

例如,关键字K1=11052501,K2=11052502,计算出K12 =122157778355001,K22 =122157800460004,取左起第7到第9位作为哈希地址。

(3)除留余数法>

这是一种用取模运算来得到哈希地址的方法。选一个合适的不大于哈希表长度的正整数P,用P去除关键字K,得到的余数作为其哈希地址。即:

H(K)=K mod P

显然,这样可以保证哈希函数的值在有效地址空间之内。该方法产生的哈希函数的好坏取决于P值的选择。若P取某个偶数,则将奇数关键字的记录映射到奇数地址上,偶数关键字的记录映射到偶数地址上,因此这样所产生的哈希地址很可能是不均匀的分布,并容易产生冲突。大量实验结果证明,当P取小于哈希表长的最大质数时,产生的哈希函数较好。这是一种最简单,也是最常用的构造哈希函数的方法。

(4)数字分析法>

设关键字有d位,对各个关键字的每位进行分析,选取关键字中取值较均匀的若干位作为哈希地址。

如图8-10所示,7个用9位十进制数表示的关键字,假设哈希表的范围为0到1000,用数字分析法确定它们的哈希地址。

图8-10 数字分析法示意图

很容易看出,第①、②、③、⑥、⑧、⑨位上重复数字很多,因此这几位不适合做哈希地址,而剩余的三位数字分布是比较均匀的,所以我们选取第④、⑤、⑦位做哈希地址,分别为:887、023、211、300、658、704、146。当然,使用这个方法构造哈希函数必须首先知道各位上的数字分布情况,这就使得该方法的使用性受到限制。

(5)随机数法>

取关键字的随机函数值作为它的哈希地址,即H(K)= random(K),其中random()为取随机数的函数。此方法通常适合于关键字长度不等的情况。

对于以上介绍的这五种构造哈希函数的方法,我们不能泛泛评价孰好孰坏,在实际应用中应当根据情况而采取不同的方法。

3.处理冲突的方法

在实际问题中,无论怎样构造哈希函数,都不可能完全避免冲突。以下介绍两种处理冲突的方法。

(1)链地址法

这种方法是把具有相同哈希地址的关键字的值存放在一个链表中。哈希表中的每个单元所存放的不是关键字的值,而是相应单链表的指针。

例8-3:设有8个元素{1,20,34,62,11,55,28,45},假设采用的哈希函数为H(K)=K mod 5,哈希表长为5时,画出用链地址法解决冲突的哈希表。如图8-11所示。

图8-11 链地址法解决冲突的哈希表

该方法适合于冲突现象比较严重的情况,它能够很好地解决溢出问题,且也很容易实现插入和删除操作。不足的是,除了哈希表所占用的存储空间外,还增加了链域空间。如果哈希函数的均匀性较差,还会造成存储单元的浪费。

(2)开放地址法

开放地址法是在冲突发生时,按照某种方法继续探测表中其它的存储单元,直到找到空位置为止。如下式所示:

Hi(K)=(H(K)+di)mod m[i=1,2,3,…,k(k≤m-1)]

其中,Hi(K)为再探测到的地址,H(K)为关键字K的直接哈希地址,m为哈希表长,di为再探测时的地址增量。

首先计算出关键字的直接哈希地址H(K)。如果该单元已经被别的元素所占用,则继续探测地址为H(K)+d1的单元,如果也被占用,再继续探测地址为H(K)+d2的单元,直到发现某个单元为空,停止探测,将记录存放到该单元中。

在这里,增量di的取法有以下几种:

(1)di =1,2,3,…;

(2)di =12,-12,22,-22,…;

(3)di =伪随机序列。

以上三种不同的取法分别称为线性探测再散列、二次探测再散列和伪随机再散列。

例8-4:有8个记录的哈希地址分别为:

H(K1)=2,H(K2)=2,H(K3)=3,H(K4)=3,

H(K5)=8,H(K6)=0,H(K7)=7,H(K8)=8。

依次将它们存入表长为10的哈希表A中。用线性探测再散列法来构造哈希表。

首先K1进入A[2]中,当K2进入时,与K1发生冲突,继续探测发现A[3]为空,因此将K2放入A[3]。当K3进入时,其直接哈希地址单元A[3]已经被K2占用,因此继续探测,A[4]为空,放入A[4]。以此类推,K4,K5,K6,K7,K8分别进入A[5],A[8],A[0],A[7],A[8]中,如图8-12所示。


图8-12 线性探测再散列法构造哈希表

在用开放地址法构造哈希表时,可能产生由非哈希函数所引起的冲突。在例8-7中,用线性探测再散列法构造哈希表时,因为K1和K2发生冲突,K2进入了A[3]单元,K3本来应该进入A[3]单元的,但是由于该单元已经被K2占用,所以必须继续向下散列到A[4]的单元中,K2与K3的冲突不是由于哈希函数引起的,而是由再散列方法本身造成的。

Last Updated: 2022/07/31 23:24:45
8小结 8.3 树表的查找