Jsoi春季函授A层次讲义一.docx

上传人:b****7 文档编号:10009096 上传时间:2023-02-07 格式:DOCX 页数:27 大小:102.70KB
下载 相关 举报
Jsoi春季函授A层次讲义一.docx_第1页
第1页 / 共27页
Jsoi春季函授A层次讲义一.docx_第2页
第2页 / 共27页
Jsoi春季函授A层次讲义一.docx_第3页
第3页 / 共27页
Jsoi春季函授A层次讲义一.docx_第4页
第4页 / 共27页
Jsoi春季函授A层次讲义一.docx_第5页
第5页 / 共27页
点击查看更多>>
下载资源
资源描述

Jsoi春季函授A层次讲义一.docx

《Jsoi春季函授A层次讲义一.docx》由会员分享,可在线阅读,更多相关《Jsoi春季函授A层次讲义一.docx(27页珍藏版)》请在冰豆网上搜索。

Jsoi春季函授A层次讲义一.docx

Jsoi春季函授A层次讲义一

哈希表及其应用

常州市第一中学林厚从

hc.lin@

一、引入

现在要存储和使用下面的线性表:

A=(1,75,324,43,1353,90,46)。

那么,很简单,定义一个一维数组A[1..n],此处n=7,将表中元素按先后顺序存储在A[i]中,但这样给查找带来了开销,尤其是n很大时,我们需要用O(n)的时间去查找某个元素(当然也可采用二分查找提高效率);反之,为了用O

(1)的时间实现查找,可以分析这个线性表的元素类型和范围,开一个一维数组A[1..1353],使得A[key]=key,即线性表的key这个元素存储在A[key]中,这样一来,查找的效率便为O

(1)了,但显然造成了空间上的很大浪费,尤其是数据范围分布很广时。

为了使空间开销减少,我们可以对第二种方法加以优化,设计一个函数h(key)=keymod13,然后把key存在A[h(hey)]中,这样一来定义一个一维数组A[0..12]就已足够,这种方法就是我们要学习的哈希表(散列表)。

哈希表是一种高效的数据结构。

它的最大优点就是把数据存储和查找所消耗的时间大大降低,几乎可以看成是常数时间;而代价仅仅是消耗比较多的内存。

然而在当前可利用内存越来越多、程序运行时间控制的越来越短的情况下,用空间换时间的做法还是值得的。

另外,哈希表编码实现起来比较容易也是它的优点之一。

二、基本原理

哈希表的基本原理是:

使用一个下标范围比较大的数组A来存储元素,设计一个函数h,对于要存储的线性表的每个元素node,取一个关键字key,算出一个函数值h(key),把h(key)作为数组下标,用A[h(key)]这个数组单元来存储node。

也可以简单的理解为,按照关键字为每一个元素“分类”,然后将这个元素存储在相应“类”所对应的地方(这一过程称为“直接定址”)。

但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了“冲突”,换句话说,就是把不同的元素分在了相同的“类”之中。

例如,假设一个结点的关键码值为key,把它存入哈希表的过程是:

根据确定的函数h计算出h(key)的值,如果以该值为地址的存储空间还没有被占用,那么就把结点存入该单元;如果此值所指单元里已存了别的结点(即发生了冲突),那么就再用另一个函数I进行映象算出I(h(key)),再看用这个值作为地址的单元是否已被占用了,若已被占用,则再用I映象,……,直到找到一个空位置将结点存入为止。

当然这只是解决“冲突”的一种简单方法,如何避免、减少和处理“冲突”是使用哈希表的一个难题。

在哈希表中查找的过程与建立哈希表的过程相似,首先计算h(key)的值,以该值为地址到基本区域中去查找。

如果该地址对应的空间未被占用,则说明查找失败,否则用该结点的关键码值与要找的key比较,如果相等则检索成功,否则要继续用函数I计算I(h(key))的值,……。

如此反复到某步或者求出的某地址空间未被占用(查找失败)或者比较相等(查找成功)为止。

三、基本概念和简单实现

图1用哈希函数h将关键字映射到哈希表T中的示意图

图1形象地表示了哈希表处理的各个要素,具体概念如下:

1、两个集合:

U是所有可能出现的关键字集合;K是实际存储的关键字集合。

2、函数h将U映射到表T[0..m-1]的下标上,可以表示成h:

U→{0,1,2,...,m-1},通常称h为“哈希函数(HashFunction)”,其作用是压缩待处理的下标范围,使待处理的|U|个值减少到m个值,从而降低空间开销(注:

|U|表示U中关键字的个数,下同)。

3、将结点按其关键字的散列地址存储到哈希表(散列表)中的过程称为“散列(Hashing)”。

方法称为“散列法”。

4、h(Ki)(Ki∈U)是关键字为Ki的结点的“存储地址”,亦称散列值、散列地址、哈希地址。

5、用散列法存储的线性表称为“哈希表(HashTable)”,又称散列表。

图中T即为哈希表。

在散列表里可以对结点进行快速检索(查找)。

6、对于关键字为key的结点,按照哈希函数h计算出地址h(key),若发现此地址已被别的结点占用,也就是说有两个不同的关键码值key1和key2对应到同一个地址,即h(key1)=h(key2),这个现象叫做“冲突(碰撞)”。

碰撞的两个(或多个)关键码称为“同义词”(相对于函数h而言)。

如图1中的关键字k2和k5,h(k2)=h(k5),即发生了“冲突”,所以k2和k5称为“同义词”。

假如先存了k2,则对于k5,我们可以存储在h(k2)+1中,当然h(k2)+1要为空,否则可以逐个往后找一个空位存放。

这是另外一种简单的解决冲突的方法。

发生了碰撞就要想办法解决,必须想办法找到另外一个新地址,这当然要降低处理效率,因此我们希望尽量减少碰撞的发生。

这就需要分析关键码集合的特性,找适当的哈希函数h使得计算出的地址尽可能“均匀分布”在地址空间中。

同时,为了提高关键码到地址转换的速度,也希望哈希函数“尽量简单”。

然而对于各种取值的关键码而言,一个好的哈希函数通常只能减少碰撞发生的次数,无法保证绝对不产生碰撞。

因此散列除去要选择适当的哈希函数以外,还要研究发生碰撞时如何解决,即用什么方法存储同义词。

7、负载因子

我们把h(key)的值域所对应到的地址空间称为“基本区域”,发生碰撞时,同义词可以存放在基本区域还没有被占用的单元里,也可以放到基本区域以外另开辟的区域中(称为“溢出区”)。

下面引入散列的一个重要参数“负载因子或装填因子(LoadFactor)”,它定义为:

а=

负载因子的大小对于碰撞的发生频率影响很大。

直观上容易想象,а越大,散列表装得越满,则再要载入新的结点时碰上已有结点的可能性越大,冲突的机会也越大。

特别当а>1时碰撞是不可避免的。

一般总是取а<1,即分配给散列表的基本区域大于所有结点所需要的空间。

当然分配的基本区域太大了也是浪费。

例如,某校学生干部的登记表,每个学生干部是一个结点,用学号做关键码,每个学号用7位数字表示,如果分配给这个散列表的基本区域为107个存储单元,那么散列函数就可以是个恒等变换,学号为7801050的学生结点就存入相对地址为7801050的单元,这样一次碰撞也不会发生,但学校仅几百个学生干部,实际仅需要几百个单元的空间,如果占用了107个存储单元,显然太浪费了,所以这是不可取的。

负载因子的大小要取得适当,使得既不过多地增加碰撞,有较快的检索速度,也不浪费存储空间。

下面结合引例说明一下上面的思想和方法。

【例1】用散列存储线性表:

A=(18,75,60,43,54,90,46)。

分析:

假定选取的散列函数为:

h(K)=Kmodm,K为元素的关键字,m为散列表的长度,用余数作为存储该元素的散列地址。

这里假定K和m均为正整数,并且m要大于等于线性表的长度n。

此例n=7,故假定取m=13,则得到的每个元素的散列地址为:

h(18)=18mod13=5h(75)=75mod13=10h(60)=60mod13=8

h(43)=43mod13=4h(54)=54mod13=2h(90)=90mod13=12

h(46)=46mod13=7

根据散列地址按顺序把元素存储到散列表H(0:

m-1)中,存储映象为:

0123456789101112

H

54

43

18

46

60

75

90

当然这是一个比较理想的情况。

假如再往表中插入第8个元素30,h(30)=30mod13=4,我们会发现H[4]已经存了43,此时就发生了冲突。

我们可以从H[4]往后按顺序找一个空位置存放30,即可以把它插入到H[6]中。

四、哈希函数的构造方法

选择适当的哈希函数是实现散列的重中之重,构造哈希函数有两个标准:

简单和均匀。

简单是指哈希函数的计算要简单快速;均匀是指对于关键字集合中的任一关键字,哈希函数能以等概率将其映射到表空间的任何一个位置上。

也就是说,哈希函数能将子集K随机均匀地分布在表的地址集{0,1,...,m-1}上,以使冲突最小化。

为简单起见,假定关键码是定义在自然数集合上,常见的哈希函数构造方法有:

1、直接定址法

以关键字Key本身或关键字加上某个数值常量C作为散列地址的方法。

散列函数为:

h(Key)=Key+C,若C为0,则散列地址就是关键字本身。

2、除余法

选择一个适当的正整数m,用m去除关键码,取其余数作为地址,即:

h(Key)=Keymodm,这个方法应用的最多,其关键是m的选取,一般选m为小于某个区域长度n的最大素数(如例1中取m=13),为什么呢?

就是为了尽力避免冲突。

假设取m=1000,则哈希函数分类的标准实际上就变成了按照关键字末三位数分类,这样最多1000类,冲突会很多。

一般地说,如果m的约数越多,那么冲突的几率就越大。

简单的证明:

假设m是一个有较多约数的数,同时在数据中存在q满足gcd(m,q)=d>1,即有m=a*d,q=b*d,则有以下等式:

qmodm=q–m*[qdivm]=q–m*[bdiva]。

其中,[bdiva]的取值范围是不会超过[0,b]的正整数。

也就是说,[bdiva]的值只有b+1种可能,而m是一个预先确定的数。

因此上式的值就只有b+1种可能了。

这样,虽然mod运算之后的余数仍然在[0,m-1]内,但是它的取值仅限于等式可能取到的那些值。

也就是说余数的分布变得不均匀了。

容易看出,m的约数越多,发生这种余数分布不均匀的情况就越频繁,冲突的几率越高。

而素数的约数是最少的,因此我们选用大素数。

记住“素数是我们的得力助手”。

3、数字分析法

常有这样的情况:

关键码的位数比存储区域的地址的位数多,在这种情况下可以对关键码的各位进行分析,丢掉分布不均匀的位留下分布均匀的位作为地址。

本方法适用于所有关键字已知,并对关键字中每一位的取值分布情况作出了分析。

【例2】对下列关键码集合(表中左边一列)进行关键码到地址的转换,要求用三位地址。

Key

H(Key)

000319426

326

000718309

709

000629443

643

000758615

715

000919697

997

000310329

329

分析:

关键码是9位的,地址是3位的,需要经过数字分析丢掉6位。

丢掉哪6位呢?

显然前3位是没有任何区分度,第5位1太多、第6位基本都是8和9、第7位都是3、4、5,这几位的区分度都不好,而相对来说,第4、8、9位分布比较均匀,所以留下这3位作为地址(表中右边一列)。

4、平方取中法

将关键码的值平方,然后取中间的几位作为散列地址。

具体取多少位视实际要求而定,取哪几位常常结合数字分析法。

【例3】将一组关键字(0100,0110,1010,1001,0111)平方后得(0010000,0012100,1020100,1002001,0012321),若取表长为1000,则可取中间的三位数作为散列地址集:

(100,121,201,020,123)。

5、折叠法

如果关键码的位数比地址码的位数多,而且各位分布较均匀,不适于用数字分析法丢掉某些数位,那么可以考虑用折叠法。

折叠法是将关键码从某些地方断开,分关键码为几个部分,其中有一部分的长度等于地址码的长度,然后将其余部分加到它的上面,如果最高位有进位,则把进位丢掉。

一般是先将关键字分割成位数相同的几段(最后一段的位数可少一些),段的位数取决于散列地址的位数,由实际需要而定,然后将它们的对应位叠加和(舍去最高位进位)作为散列地址。

【例4】如关键码Key=58422241,要求转换为3位的地址码。

分析:

分如下3段:

584|222|41,则相加:

584

222

41

847

h(Key)=847

6、基数转换法

将关键码值看成在另一个基数制上的表示,然后把它转换成原来基数制的数,再用数字分析法取其中的几位作为地址。

一般取大于原来基数的数作转换的基数,并且两个基数要是互质的。

如:

key=(236075)10是以10为基数的十进制数,现在将它看成是以13为基数的十三进制数(236075)13,然后将它转换成十进制数。

(236075)13=2*135+3*134+6*133+7*13+5

=(841547)10

再进行数字分析,比如选择第2,3,4,5位,于是h(236075)=4154

五、哈希表支持的运算

哈希表支持的运算主要有:

初始化(makenull)、哈希函数值的运算(h(x))、插入元素(insert)、查找元素(member)。

设插入元素的关键字为x,A为哈希表,则各种运算过程如下:

1、初始化比较容易,例如:

constempty=maxlongint;{用非常大的整数代表这个位置没有存储元素}

p=9997;{根据需要设定的表的大小}

proceduremakenull;

vari:

integer;

begin

fori:

=0top-1doA[i]:

=empty;

End;

2、哈希函数值的运算,根据函数的不同而变化,例如除余法的一个例子:

functionh(x:

longint):

Integer;

begin

h:

=xmodp;

end;

3、我们注意到,插入和查找首先都需要对这个元素定位,因此加入一个定位的函数locate:

functionlocate(x:

longint):

integer;

varorig,i:

integer;

begin

orig:

=h(x);

i:

=0;

while(ix)and(A[(orig+i)modP]<>empty)doinc(i);

{当这个循环停下来时,要么找到一个空的存储单元,

要么找到这个元素存储的单元,要么表已经满了}

locate:

=(orig+i)modP;

end;

4、插入元素:

procedureinsert(x:

longint);

varposi:

integer;

begin

posi:

=locate(x);{定位函数的返回值}

ifA[posi]=emptythenA[posi]:

=x

elseerror;{error即为发生了错误,当然这是可以避免的}

end;

5、查找元素是否已经在表中:

proceduremember(x:

longint):

boolean;

varpos:

integer;

begin

pos:

=locate(x);

ifA[pos]=xthenmember:

=true

elsemember:

=false;

end;

 

六、处理冲突的方法

1、完全避免冲突的条件

最理想的解决冲突的方法是完全避免冲突。

要做到这一点必须满足两个条件:

一是|U|≤m;二是选择合适的散列函数。

这仅适用于|U|较小,且关键字均事先已知的情况,此时经过精心设计散列函数h有可能完全避免冲突。

但是,一味的追求低冲突率也不好。

理论上是可以设计出一个几乎完美、几乎没有冲突的哈希函数。

然而,这样做显然不值得,因为这样的函数设计很浪费时间而且编码一定很复杂,与其花费这么大的精力去设计函数,还不如用一个虽然冲突多一些但是编码简单的哈希函数。

所以,设计一个好的哈希函数是很关键的。

而“好”的标准,就是较低的冲突率和易于实现。

2、冲突不可能完全避免

通常情况下,哈希函数h是一个压缩映像。

虽然|K|≤m,但|U|>m,故无论怎样设计h,也不可能完全避免冲突。

因此,只能在设计h时尽可能使冲突最少。

同时还需要确定解决冲突的方法,使发生冲突的同义词能够存储到表中。

处理冲突的方法基本上有两类:

一类方法是“拉链法(Chaining)”,当发生冲突时就拉出一条链,建立一个链接方式的子表。

若n个关键码值映象到基本区域的m个存储单元上,最多可以建立m个子表,每个关键码的同义词存放在以这m个单元为首结点链接的子表里。

另一类叫“开地址法(OpenAddressing)”,也称为“开放定址法”。

当冲突发生时,用某种方法在基本区内形成一个探查序列,沿着这个探查序列一个单元一个单元地查找,直到找到这个关键码或碰到一个开放的地址(没有存储关键码的空单元)为止,下面分别讲解。

3、拉链法

用拉链法法处理冲突时要求散列表的每个结点增加一个link字段,用于链接同义词子表。

同义词子表建立在什么地方?

一种办法是在基本存储区外开辟一个溢出区存储同义词表,这种方法叫做建立“分离的同义词表”的方法。

这时基本存储区里的结点即存放关键码值,同时又是一个链接的同义词子表的表头。

如果某个关键码值没有同义词,link字段为空;如果某个关键码值有同义词,则它的link字段指向溢出区的同义词子表。

同义词的检索也是顺着这些链进行的。

这种方法的存储如图2所示。

图2用分离的同义词子表解决冲突

【例5】已知一组关键字为(26,36,41,38,44,15,68,12,06,51),请用拉链法解决冲突,构造这组关键字的散列表。

分析:

这里关键字个数n=10,为了减少冲突(保证装填因子а

不妨取m=13,此时α≈0.77,散列表为T[0..12],散列函数为:

h(k)=kmod13。

具体构造过程参见动画模拟软件llf.swf(注:

请安装flash或播放软件)。

注意:

 当把h(k)=i的关键字插入第i个单链表时,既可插入在链表的头上,也可以插在链表的尾上。

若采用将新关键字插入链尾的方式,依次把给定的这组关键字插入表中,则所得到的散列表如下图3所示,具体程序留给大家完成。

 

另一个办法是不另外建立溢出区,而是把同义词子表就存入基本存储区中目前还没有被占用的单元里,例如:

可以在基本存储区里从后往前找空单元,找到空单元就将同义词存进去,并将它链接进同义词子表。

这种方法叫建立“结合的同义词子表”。

下面给出该方法的算法。

此算法在散列表里检索一个给定的关键码值,设此关键码值进入算法前已在变量K中,若在散列表中找到这个关键码值则检索成功;若找不到则将这个关键码插入散列表。

算法用散列表函数h(x)计算表里的相对地址,有关的类型和变量说明如下:

TYPEnode=RECORD

key:

integer;

link:

integer;

END

VAR

table:

ARRAY[0..m-1]OFnode;

r,k,i:

integer;

r是个辅助变量,用来帮助在插入时找到可利用单元。

r的初始值为m。

在算法进行过程中,始终有下列情况存在:

散列表中从table[r]到table[m-1]的所有单元都已被占用了。

设未被占用的单元起始内容全为0,而关键码值不能为0。

【算法1】 散列表的检索和插入(用结合的同义词子表解决冲突)

begin

i:

=h(k);{计算散列地址}

iftable[i].key=0{检索并插入}

thenbegintable[i].key:

=k;table[i].link:

=-1;write(‘inseted’,i);end

elsebegin

while((table[i].key<>k)and(table[i].link<>-1))do

I:

=table[i].link;

Iftable[i].key=kthenwrite(‘retreval’,i,table[i])

  Elsebegin

While((r>=0)and(table[r].key<>0))do r:

=r-1;

Ifr<0thenwrite(‘overflow’)

Elsebegin

table[i].link:

=r;

table[r].key:

=k;

table[r].link:

=-1;

write(‘inserted’,r);

end;

end;

end;

end;

4、开地址法

用开地址法解决冲突的做法是:

当冲突发生时一定要产生一个探查序列,检索沿这个探查序列去查找。

最简单的产生探查序列的方法是进行线性探查,就是当冲突发生时到顺序的下一个基本存储区单元进行探查。

将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(Key)=d),则最长的探查序列为:

d+l,d+2,...,m-1,0,1,...,d-1

即:

探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],...,直到T[m-1],此后又循环到T[0],T[1],...,直到探查到T[d-1]为止。

探查过程终止于三种情况:

(1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);

 

(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;

(3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。

利用开地址法的一般形式,线性探查法的探查序列为:

I(i)=(h(Key)+i)modm(0<i≤m-1=

下面具体介绍:

(1)利用线性探查法构造散列表

【例6】已知一组关键字和选定的散列函数和例5相同,用除余法构造散列函数,用线性探查法解决冲突,构造这组关键字的散列表。

分析:

和例1类似,散列函数为:

h(K)=Kmod13,散列表为T[0..12]。

由除余法的散列函数计算出的上述关键字序列的散列地址为(0,10,2,12,5,2,3,12,6,12)。

前5个关键字插入时,其相应的地址均为开放地址,故将它们直接插入T[0],T[10],T[2],T[12]和T[5]中。

当插入第6个关键字15时,其散列地址2(即h(15)=15mod13=2)已被关键字41(15和41互为同义词)占用。

故探查I=(2+1)mod13=3,此地址开放,所以将15放入T[3]中。

当插入第7个关键字68时,其散列地址3已被非同义词15先占用,故将其插入到T[4]中。

当插入第8个关键字12时,散列地址12已被同义词38占用,故探查I1=(12+1)mod13=0,而T[0]亦被26占用,再探查I2=(12+2)mod13=1,此地址开放,可将12插入其中。

类似地,第9个关键字06直接插入T[6]中;而最后一个关键字51插人时,因探查的地址12,0,1,……,6均非空,故51插入T[7]中。

用开地址法(线性探查法)解决冲突,构造散列表的具体过程参见动画模拟软件kfdzh.swf。

下面给出用线性探查法解决冲突的算法,算法用散列函数h(x)计算散列地址。

算法有关类型和变量说明如下:

TYPEnode=RECORD

key:

integer

END;

VAR

table:

ARRAY[0…N-1]OFnode;

k,i,j:

integer;

j是个辅助变量,用来记录表里已有多少个表目,散列表建立时,设基本存储区已清为0,且j=0,要检索或插入的关键码,在进入算法前已放在变量K之中,算法中的m是一个常量,等于基本存储区的长度。

【算法2】散列表的检索和插入(用线性探索法解决冲突)

begin

I:

=h(k);{计算散列地址}

while((table[I].key<>k)and(table

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 工程科技 > 纺织轻工业

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1