算法及数据结构之散列表

基本概念

散列表是支持INSERT、DELETE和SEARCH的字典操作,其是对普通数组概念的推广,因为可以对数组元素进行直接寻址,故可在O(1)时间内访问数组的任意元素。

当实际存储的关键字数比可能的关键字总数较小时,这时采用散列表比直接的数组寻址更为有效,因为散列表通常采用的数组尺寸与索要存储的关键字数是成比例的。在散列表中,根据关键字计算出数组下标。


直接寻址表

当关键字的全域U比较小并且任意两个关键字都不相同时,其中每个关键字元素取自全域U={0,1,...,m-1},此处m是一个不大的数,我们用一个数组(寻址表)T[0...m-1],其中每个位置对应全域U中的一个关键字。在这个表中进行字典操作的执行是很快的,只需O(1)的时间。


散列表

直接寻址技术一个明显的问题是,如果域U很大,在一台典型计算机的可用内存容量限制下,为其设计一个对应的直接寻址表T是不太现实的。

当实际存储在字典中的关键字集合K比所有可能的关键字域U小得多时,我们使用散列表。

在直接寻址方式下,具有关键字k的元素被存放到槽k中。在散列方式下,该元素处于h(k)中,也就是利用散列函数h,根据关键字k计算出槽的位置,函数h将关键字映射到散列表T[0...m-1]的槽位上。

h:U→{0...m-1}

采用散列函数的目的就在于缩小需要处理的小标范围,即要处理的值从|U|降到了m,从而降低了空间开销。

这样做存在一个问题,就是两个关键字很可能映射到同一个槽上,我们将这种情形成为发生了碰撞。


解决碰撞

通过链表法解决碰撞

在链表法中,把散列到同一个槽中的所有元素都放在一个链表中。如槽j中有一个指针,它指向由所有散列到j的元素构成的链表的投,如果不存在这样的元素,则j中为NIL。


用链表法散列的最坏情况是所有的关键字n都散列到同一个槽中,从而产生出一个长度为n的链表,这时最坏情况下查找的时间为O(n)。散列方法的平均态依赖于所选取的散列函数h在一般情况下,将所有关键字分不在m个槽位上的均匀程度。假定任何元素散列到m个槽中的每一个的可能性相同,且与其他元素已被散列到什么位置上是独立无关的,这个假定称为简单一致散列。


散列函数

一个好的散列函数应或近似地满足简单一致散列的假设。

将关键字解释为自然数:多数散列函数都假定关键字域为自然数集N={0,1,2,...},如果给定的关键字不为自然数,则必须有一种方法来将他们解释为自然数。


除法散列法

通过取k除以m的余数,将关键字k映射到m个槽的某一个中去,即散列函数:

h(k)=k mod m

对于m的选择常常为与2的整数幂不太接近的质数。


乘法散列法

构造散列函数的乘法方法包含两个步骤,首先用关键字k乘上常数A(0<A<1),并抽取出kA的小数部分,然后用m诚意这个值,再取结果的底,散列函数为:

h(k)=floor(m(kA mod 1))

其中kA mod 1为kA的小数部分,对于A的选择一般为:A约等于(√5-1)/2,而m一般选择为2的某个幂次。


全域散列

任何一个特定的散列函数都有可能出现最坏的情况,使得平均检索时间为O(n),唯一有效的改进方法是随机地选择散列函数,使之独立于要存储的关键字,这种方法成为全域散列。

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


开放寻址法

在开放寻址法中,所有的元素都存放在散列表理,亦即每个表项或包含动态集合的一个元素或为NIL。当查找一个元素时,要检查所有的表项,直到找到所需的元素或发现元素不在表中。开放寻址法中的装在因子不能超过1。

在开放寻址法中,当插入一个元素时,可以连续检查散列表的各项,直到找到一个空槽来放置待插入的关键字为止。检查的顺序不一定为0,1,2...,而是要依赖待插入的关键字,为了确定要探查哪些槽,我们将散列函数加以扩充,使之包含探查号(从0开始)以作为其第二个输入参数,如下面伪代码:

HASH-INSERT(T,k)

← 0

repeat j ← h(k,i)

if T[j] = NIL

then T[j] ← k

return j

else i ← i + 1

until i = m

error "hash table overflow"

查找关键字k的算法的探查序列与将k插入时的插入算法一样,当查找到关键字或查找过程中碰到一个空槽时,查找算法就停止。

在开放寻址法中,当我们从槽i中删除关键字时,不能仅将NIL置于其中来标志它为空,如果这样的话,就会有个问题:在插入某关键字k的探查过程中,发现i被占用了,则k就被插入到后面的位置上,在将槽i中的关键字删除后,就无法检索关键字k了。有一个办法就是在槽i中置一个特定的值DELETED,而不是NIL。这样HASH-INSERT做相应调整,使得DELETED标志的槽位仍可放入新的元素。

有三种技术常用来计算开放寻址法中的探查序列:线性探查、二次探查以及双重探查。


线性探查

首先给定一个普通的散列函数h1(k),则线性探查的散列函数为h(k,i)=(h1(k)+i) mod m,i=0,1,...,m-1。

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


二次探查

二次探查采用如下形式的散列函数:h(k,i)=(h1(k)+c1*i+c2*i2) mod m,其中h1是一个普通散列函数,c1、c2为常数,i=0,1,...,m-1。如果两个关键字的初始探查位置相同,那么它们的探查序列也是相同的,这一性质可导致一种程度较轻的群集现象,称为二次群集。


双重散列

双重散列所产生的排列具有随机选择的排列的许多特性,它采用如下形式的散列函数:h(k,i)=(h1(k)+ih2(k)) mod m,其中h1和h2为普通散列函数,初始探查位置为T[h1(k)],后续探查位置在此基础上加上偏移量h2(k),这里的探查序列以两种方式依赖于关键字k,因为初始探查位置、偏移量都可能发生变化。


完全散列

当关键字集合是静态的时(指各关键字存入表中后,关键字集合就不再变化了)。

如果某一种散列技术在进行查找时,其最坏情况内存访问次数为O(1)的话,则称其为完全散列。

设计一种完全散列:我们利用一种两级的散列方案,每一级都采用全域散列。

第一级与带链接的散列基本上是一样的:利用从某一全域散列函数族中仔细选出一个散列函数h,将n个关键字映射到m个槽中。

对于第二级,我们不是对散列到槽j中的所有关键字建立一个链表,而是采用了一个较小的二次散列表S,与其相关的散列函数h1,可以确保在第二级上不出现碰撞。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值