您的位置:首页 > 理论基础 > 数据结构算法

数据结构笔记(1)散列表

2015-10-23 09:08 281 查看
在很多应用中,都要用到一种动态集合结构,它仅支持insert、search、delete字典操作。如计算机程序语言设计的编译程序中需要维护一个符号表,其中元素的关键字值为任意字符串,与语言中的标识符相对应。实现字典的一种有效数据结构为散列表(hash table)。在最坏的情况下,在散列表中,查找一个元素和在链表中查找一个元素的时间相同,最坏的情况都是Θ(n)。但是在实践中,散列技术的效率实际上是很高的,在一些合理的假设下,在散列表中查找一个元素的期望时间为O(1)。下面介绍几种常用的散列表和散列技术:

1. 直接寻址表

当关键字的全域U比较小时,直接寻址是一种简单有效的技术。假设存在动态集合D,其中每个元素都来自全域U={0,1,……m-1}的关键字,m是一个不大的数,假设任意两个元素关键字都不相同。为表示动态集合D,我们用一个直接寻址表(数组)T[0…,m-1],其中每个位置k对应全域U的一个关键字。如果集合中没有关键字为k的元素T[k]=null.



动态集合的元素也可以放在直接寻址表中,在c++中通过malloc申请空间,获取基地址,k作为偏移量加基地址构成内存地址。

直接寻址表也存在明显的问题:(1)如果U很大,在有计算机内存容量限制的条件下,产生一张直接内存表不太实际。(2)如果关键字集合K相对U很小,那么分配的空间就会很大部分被浪费掉。

2、散列表

当存储在字典中的关键字集合K比所可能的关键字域小的对视,散列表需要存储的空间比直接寻址要少好多。

在直接寻址方式瞎,具有关键字k的元素被存放在槽k中。在散列方式下,该元素处于h(k)中,即利用散列函数h,根据关键字k计算出槽的位置.

函数h将关键字域U映射到[0,……m-1]的槽位上:

h:U->{0,1……,m-1}



但是这样做也会碰到一个问题:两个关键字可能会映射到同一个槽上,我们将这种情况称为碰撞(collision)。而碰撞的产生给查找删除等带来了难度,所以我们需要办法来解决碰撞。当然最理想的方法是完全避免碰撞但是由于|U|>m,即必有两个元素的散列关键值k相同,所以想完全避免碰撞是不可能的。所以一方面我们可以研究解决碰撞带来问题的方法,另一方面,通过精心设计的随机散列函数尽可能的减少碰撞。

下面我们首先介绍一种最简单的碰撞解决技术,链接法。在后文我们还将介绍其他的碰撞解决方法。

链接法解决碰撞

在链接法中,把散列到同一个槽的所有元素都放在一个链表中。槽中有一个指针,它指向所有散列到该槽中的元素说构成的链表的头,如果没有元素,该槽为null.



由于每次插入元素都是在表头插入,所以插入操作的最坏运行时间为O(1),删除操作运行的时间与表的长度成正比。

对用链接法散列的分析

给定一个能存放n个元素,具有m个槽位的散列表T,定义装载因子a为n/m即一个链中平均存储的元素数。

链表法散列的最坏情况性功能很差,如n个元素都在同一个槽位中,那么就与普通的链表在查找上没有太大区别,显然,我们并不会因为散列最坏情况下性能差而不去使用它。

散列的平均性态取决于所选取的散列函数h在一般情况下,将所有关键字分布在m个槽位上的均匀程度。假设任何元素散列到m个槽位中的可能性都是相同的,且与其他元素散列到什么位置上是无关的,称这个假设为 简单一致散列

对于j=0,1,2……,m−1,列表T[j]的长度用nj表示,这样有n=n0+n1+……nm−1

nj的平均期望值为E[nj]=a=n/m

定理 对于一个链接技术解决碰撞的散列表,在简单一致散列的假设下,一次不成功查找的期望时间为Θ(1+a),对于一次成功查找来说情况略有不同。

在简单一致散列假设下,对用链接技术解决碰撞的散列表,平均情况下一次成功查找也需要Θ(1+a)时间

证明:

假设要查找的关键字表中存放的n个关键字任何一个的可能性都是相同的。对于元素x的一次成功查找中,所检查的元素比x前面的元素多1.为了确定所查找元素期望数目,对在x之后插入表中的期望加1,再取平均。设xi表示插入到表中的第i个元素,i=1,2……n,并设ki=key[xi].对于关键字ki和kj,定义指示器随机变量Xij=I{h(ki)=h(kj)},在简单一致散列假设条件下Pr{h(ki)=h(kj)}=1/m,有此E[Xij]=1/m.所以在一次成功的查找中,所检查元素的期望为



这一结论说明散列表中的槽数与表中的元素数成正比,n=O(m),从而a=n/m=O(m)/m=O(1),说明平均查找操作需要常数量时间,从而散列表所有操作都可以在O(1)内完成。

3.散列函数

在这里我们介绍三种散列函数的设计方案。

好的散列函数特点

一个好的散列函数应该满足简单一致散列假设:每个关键字都可能散列到m个槽位的的任意一个中去,并且与被散列到哪个槽位中无关。

然而,一般情况下并不太可能检查这一条件是否成立,而且个关键字之间不可能完全独立。在实践中常常用启发式的技术来构造好的散列函数。

将关键字解释为自然数

多数散列函数都是假定关键字域为自然数集N={0,1,2⋅⋅⋅}.如果关键字域不是自然数,必须想办法将它解释成自然数。如pt在ASCII码中为112,116则pt=(112*128)+116=14452.在任意给定应用中,通常比较容易设计出类似的方法将每个关键字解释为自然数。在后面的内容中,假定所给的关键字都是自然数。

散列函数设计

(1)除法散列法

在用来设计散列函数的除法散列法中,通过k除以m的语数,来将关键字k映射到m个槽中的某一个去,亦即,散列函数为:h(k)=k mod m

这种方法只需要一次除法操作所以比较快。

注意:当应用除法散列时,要注意m的选择。例如m最好不应该为2的幂,如果m=2p,则h(k)就是k的p个低位数字。除非我们事先知道,关键字的概率分布使得k的各种最低位p的排列方式尽可能相同。

(2)乘法散列法

构造散列函数的乘法方法包含两个步骤。第一步用关键字k乘上常数A(0<A<1),并且抽取kA的小数部分。然后用m乘以这个值,再取结果的底。总之,散列函数为h(k)=⌊m(kA mod 1)⌋,其中kA mod 1即为kA的小数部分,亦即kA−⌊kA⌋。

乘法方法的一个优点是对m的选择没什么要求,一般选择它的2为底的某个幂次(m=2p,p为某个整数)。因为我们可以在大多数计算机上很简单的实现散列函数。假设计算机的字长为w位,而k刚好可以容于一个字中。使A为形如s/2w的一个分数,其中s是一个取自0<s<2w中的整数,参照下图先用w位整数s=A∗2w乘上k,其结果为2w位的r12w+r0,其中r1为乘积的高位字,r0为乘积的低位字。所求的p位散列值中包含了r0的p个最高有效位。



虽然这个方法对任何A值都适用,但对某些值效果特别好。最佳的选择与待散列的数据的特征有关Knuth认为A=(5√−1)/2 = 0.6180339887⋅⋅⋅就是一个比较理想的值。

(3 )全域散列

任何一个特定的散列函数都可能出现最坏的情况性态:唯一有效的改进方法是随机的选择散列函数,使之独立于存储的关键字。这种方法称为全域散列(universal hashing),不管选择怎样的关键字,其平均性态都很好。

全域散列的基本思想是在执行开始时,就从一族仔细设计的函数中,随机选择一个作为散列函数,就像在快速排序中一样,随机化保证了没有哪一种输入会始终导致最坏情况性态。同时随机化使得即使对同一个输入,算法在每一次执行的性态也不一样,这样就可以确保对于任何输入,算法都有较好的平均性态。

设Η为有限的一组散列函数,将给定的关键字域U映射到{0,1⋅⋅⋅,m−1}中。这样的一组函数称为全域的,如果对于每一个不同的关键字k,l∈U,满足h(k)=h(l)的散列函数h∈H的个数最多为|H|/m。换言之,如果从H中随机选择一个散列函数,当关键字k≠l时,两者碰撞的概率不大于1/m,这也正好是从集合{0,1…,m-1}中随机的独立的选择h(l)和h(k)时发生碰撞的概率。从下面的定理中我们可以知道,全域散列函数类的平均性态还是比较好的。

定理 如果h选自一组全域的散列函数,并将n个关键词散列到一个大小为m的,用链接法解决碰撞的表T中。如果关键字k不在表中,则k被散列到其中链表的期望长度E[nh(k)]至多为a.如果关键字k在表中,则包含k关键字的链表的期望长度E[nh(k)]至多为1+a



n/m=a



4.开放寻址法解决碰撞问题

在开放寻址法中,所有的元素都存放在散列表里。亦即,每个表项要么包含一个元素,要么为空。当检查一个元素时要检查所有的表项,知道找到所需要的元素,或者发现元素不在表中。散列表可能会被填满,但装载因子a绝对不会超过1。

开放寻址法的好处是不需要指针,而是计算出要存取的各个槽,由于不使用指针而节省了空间可以放更多的槽,潜在效果就是减少碰撞,提高查找速度。

在开放寻址法中插入一个元素时,可以连续的探查散列表的各项,直到找到空槽插入关键字为止检查的顺序不一定是0,1,…m-1(这种顺序下查找时间为Θ(n)),而是依赖于待插入的关键字。为了确定要探查哪些槽,我们将散列函数加以扩充,使之包含探查号(从0开始)以作为其第二个输入的参数。这样散列函数就变为h:U×{0,1,⋅⋅⋅}→{0,1,2,⋅⋅⋅,m−1}对于开放寻址法来说要求每一个关键字k,探查序列<h(k,0),h(k,1)⋅⋅⋅,h(k,m−1)>,必须是<0,1,…,m-1>,使得当散列表逐渐填满时,每一个表位最终可以被视为用来插入新关键字的槽。

插入算法伪代码

HASH-INSERT(T,k)
{
int i=0;
while(i<=m)
{
int j=h(k,i);
if(T[j]=null)
{
T[j]=k;
return j;
break;
}else i++;
if(i=m) error(hash table overflow)
}
}


查找算法伪代码

HASH-INSERT(T,k)
{
int i=0;
while (T[j]&&i<m)
{
j=h(k,i);
if (T[j]=k)
{
return j;
break;
}else i++;
}
return null;
}


在开放寻址法中,对散列元素的删除操作执行起来比较困难。所以在必须删除关键字的应用中往往使用链表法解决碰撞。

开放寻址的探查技术

在我们的分析中,做一个一致散列的假设,即假设每个关键字的探查序列是<0,1,⋅⋅⋅m−1>的m!种排列的人以一种可能性是相同的。一致散列将前面的简单一致散列加以概化,推广到散列函数的结果不只是一个数,而是一个完整的探查序列的情形。然而在现实中真正的一致散列是很难实现的,在实践中,常常采用近似的方法如:线性探查,二次探查,双重探查。在这三种技术中双重散列能产生的探查序列数最多,因而能给出好的结果。

线性探查

给定一个普通的散列函数h′:U→ {0,1,⋅⋅⋅,m−1}(称为线性辅助散列函数),线性探查法采用的散列函数为h(k,i)=h′(k)+i) mod m i=0,1,⋅⋅⋅m−1给定一个关键字k,第一个探查的槽为T[h′(k)],亦即由散列辅助函数所给出的槽,接下来探查的是T[h′(k)+1],直到槽T[m−1],然后绕回到槽T[0]……,直到槽T[h′(k)−1]结束。线性探查中,初始探查位置确定了整个序列,故只有m种不同的探查序列。

线性探查的方法比较容易实现,但存在一个问题,称作一次群集。随着时间的推移,被连续占用的槽不断增加,平均查找时间随着不断增加。群集现象很容易出现,这是应为当一个空槽前有i个满的槽时,该空槽为下一个将被占用的槽的概率为(i+1)/m。连续被占用的槽的序列会越变越长,因而平均查找的时间也会随之增加。

二次探查

二次探查采用如下形式的散列函数:h(k,i)=(h′(k)+c1i+c2i2) mod m其中h′是一个辅助函数c1,c2(≠0)为辅助常数,i=0,1,2…m-1初始的探查位置为T[h′(k)],后续的探查位置要在此基础上加上一个偏移量,偏移量一二次的形式依赖于探查号i,这种探查方法的效果要比线性探查好很多,但是为了能够充分利用散列表,c1、c2和m的值要受到限制。此外,如果两个关键字的初始探查位置相同,那么他们的探查序列也是相同的,这是因为h(k1,0)=h(k2,0)蕴含着h(k1,i)=h(k2,i)。这一性质导致较轻的群集现象,称为二次群集。如线性探查一样,初始探查决定整个序列。

因此只有m中探查序列被用到了。

双重散列

双重散列是用于开放寻址的最好方法之一,因为它所产生的排列具有随机选择排列的许多特性,采用 如下形式的散列函数:h(k,i)=(h1(k)+i h2(k)) mod m其中h1,h2为辅助散列函数。初始探查位置为T[h1(k)],后续的探查位置在此基础上加上偏移量h2(k)并模m。与线性和线性二次探查不同的是,这里的探查序列以两种方式依赖于关键字k,因为初始探查位置、偏移量都可能发生变化。

为了能查找整个散列表,值h2(k)要与表的大小m互质。确保这一条件总是成立的一种方法是取m为2的幂,并设计一个总是产生奇数的h2。另一种方法是取m为质数,并设计一个总是产生较m小的正整数的h2。例如:我们可以取m为质数,并取h1(k)=k mod m , h2(k)=1+(k mod m′)其中m′略小于m。

举例如下:



双重散列法下的插入。此处散列表的大小为13,h1(k)=k mod 13,h2(k)=1+k mod 11。因为14=1(mod 13),且14=(3 mod 11),故探查槽1和槽5,并且发现它们被占用后14就被插入槽9中。

双重散列法中共用了Θ(m2)种探查序列,而线性探查或者二次探查中用了Θ(m)种,故前者是对后者的一种改进。这种改进的原因在于,对每一种可能的(h1(k),h2(k))都产生了不同的探查序列,因而双重散列与理想的一致散列性能看起来就很近了。

对于开放寻址散列的分析

假定散列是一致的,给定一个装载因子a=n/m<1的开放寻址散列表:

(1)在一次不成功的查找中,期望的探查次数至多为1/(1-a)。

(2)平均情况下,向开放寻址散列表里插入一个元素需要探查的次数至多为1/(1-a)

(3)一次成功查找的期望探查数至多为1aln11−a

散列就介绍道这里,还有许多东西并没有完全展现出来,但最重要的是能够灵活的使用散列,从而带来更高的效率。

参考文献

《算法导论》 Tomas H.Cormen
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  数据结构