算法导论十一章:散列表

来源:互联网 发布:2017网络彩票能重启吗 编辑:程序博客网 时间:2024/06/05 16:15

实现字典的一种有效的数据结构为散列表,在最坏的情况下,在散列表中查找一个元素的时间与在链表中查找一个元素的时间相同,为Θ(n)。在实践中,散列技术的效率是很高的,在一些合理的假设下,在散列表中查找一个元素的期望时间为O(1)。散列表是普通数组概率的推广。

 

11.1直接寻址表

 

当关键字的全域U比较小时,直接寻址时一种简单而有效的技术。假设某个动态集合都一个取自全域U={0,1,...,m-1}的关键字,假设没有两个元素具有相同的关键字。

 

我们用一个数组T[0...m-1](直接寻址表),每个位置对应全域U中的一个关键字;槽k指向集合中一个关键字为为k的元素,如果该集合没有关键位k的元素,则T[k]=NIL。

 

11.2散列表

 

如果U很大的话,直接寻址表就不适用了,而且如果存储的关键集合K相对U来说可能很小,分配的大部分空间都要浪费掉。

 

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

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

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

 

散列表有一个问题就是不同的关键字可能映射到同一个槽上,这种情况叫做碰撞。最理想的解决方法就是完全避免碰撞,选用合适的h。在选择时有一个主导思想就是h近可能地随机,从而避免或最大限度避免碰撞,术语“散列”即体现了这种精神。当|U|>m时,要完全避免碰撞时不可能的。

 

链接法解决碰撞

 

链接法,把散列到同一槽中所有元素都放在一个链表中,槽j中有一个指针,它指向所有散列到j的元素构成的链表的头。

 

给定一个能存放n个元素、具有m个槽位的散列表T,定义T的装载因子为α=n/m。

 

散列方法的平均性能依赖于所选取的散列函数h在一般情况下将所有关键字分布在m个槽位上的均匀程度。假设任意元素散列到m个槽中的每一个的可能性是相同的,且与其他元素已被散列到什么位置上是独立无关的,称这个假设为“简单一致散列”。

 

在这种情况下查找的期望时间是Θ(1+α)。

 

 11.3散列函数

这一节讨论三种散列函数设计方案,前两种都是启发式方法,第三种方法则利用了随机化的技术,提供可证明的良好性能。

 

好的散列函数的特点

 

一个号的散列函数应满足“简单一致散列的假设”。一般情况下不太可能检查这一条件是否成立,因为很少能知道关键字所符合的概率分析,而各关键字可能并不是完全相互独立的。

 

在实践中,常常可以运用启发式技术来构造好的散列函数,在设计过程中可以利用有关关键字的分布的限制信息。例如在一个编译器的符号表,关键字都是字符串,表示程序中的标志符,在同一个程序中经常会出现一些很相近的符号,比如pt和pts。好的散列函数应该能够最小化将这些相近符号散列到同一个槽中的可能性。

 

一种好的做法是独立于“数据中可能存在的任何模式”方式到处散列值。

 

将关键字解释为自然数

 

许多散列函数都假定关键字域为自然数集N。如果所给关键字不是自然数,则必须有一种方法来将它们解释为自然数。例如标识符pt可以解释为十进制整数对(112,116),ASCII字符值,然后pt即为112*128+116=14452。

 

11.3.1除法散列法

通过取k除以m的余数,来将关键字k映射到m个槽中的某一个: h(k) = k mode m。

除法散列要注意m的选择,m不应是2的幂,否则h(k)就是k的p个最低位。

 

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

11.3.2乘法散列法

构造散列函数的乘法方法包含两个步骤。第一步,用关键字k乘上常数A(0<A<1),并取出k*A的小数部分。然后用m乘以这个值,再取底。

这个方法对m的选择没有什么要求,一般选择它为2的某个幂2p。可以按如下方法实现,假设计算机的字长为w位,而k正好可以容于一个字中。限制A为形如S/2w的一个分数。先用w位整数S乘上k,其结果是一个2w位的值r1*2w+r0,其中r1为乘积的高位字,r0为低位字。r0的p个最高有效位就是h(k)。

 

Knuth认为A=(√5-1)/2=0.6180339887...是一个比较理想的值。

11.3.3全域散列

如果让某个与你作对的人来选择要散列的关键字,那么他会选择全部散列到同一个槽中的n个关键字。任何一个特定的散列函数都可能出现这种最坏情况。唯一的有效改进方法时随机地选择散列函数,使之独立于要存储的关键字。这种方法称为“全域散列”。

 

全域散列的基本思想就是在执行开始时,从一族仔细设计的函数中,随机选定一个作为散列函数。只有恰好选择了一个与即将存储的关键字形成较坏性态的散列函数,才会出现差的性能。

 

设H为有限的一族散列函数,它将关键字域U映射到{0,1,...,m-1}。这样一族函数称为全域的:对每一对不同的关键字k,l,满足h(k)=h(l)的散列函数h∈H的个数至多为|H|/m。换言之,如果随机地选择一个散列函数,两个关键字发生碰撞的概率不大于1/m。可以证明每个槽链表的期望至多为1+α。

 

设计全域散列函数集:

选一个足够大的质数p,使得每个可能的关键字k都落在0到p-1的范围内。设Zp={0,1...,p-1},设Z*p={0,1,...,p-1}。取a∈Zp,b∈Z*p,定义函数ha,b(k)= ((ak+b) mod p) mod m,Hp,m函数族共有p(p-1)个函数。

11.4 开放寻址法

解决碰撞的另一张方法就是开放寻址法,所有的元素都放在散列表槽中。当要插入一个元素时,可以连续地检查散列表的各项,知道找到一个空槽。检查的顺序不一定是{0,1,...,m-1}。将散列函数加以扩充,使之包含探查号作为第二个输入参数,散列函数变成: h: U x {0,1,...,m-1} --> {0,1,...,m-1}。

对开放寻址法来说,要求每个关键字的探查序列 { h(k,0), h(k,1),...,h(k,m-1)}必须是{0,1,...,m-1}的一个排列。

 

HASH-INSERT(T,k)

1   i<--0

2   repeat j <-- h(k,i)

3        if T[j]=NIL

4           then  T[j]<--k

5                return j

6           else i<--i+1

7    until i=n

8   error "hash table overflow"

 

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

 

线性探查

    给定一个普通的散列函数h'(k), 线性探查采用的散列函数为h(k,i) = (h'(k)+i) mod m。

    线性探查存在“一次群集”现象。当一个空槽前有i个满槽时,该空槽为下一个将被占用槽的概率为(i+1)/m。连续被占用的槽会越来越长,因而平均查找时间会随之增加。

 

二次探查

“二次探查”采用如下形式散列函数

   h(k,i) = (h'(k) + c1i + c2i2) mod m

 

双重散列

双重散列是用于开发寻址法德最好方法之一,它产生的排列具有随机选择的许多特性,它采用如下形式的散列函数:

 h(k,i)  = ( h1(k) + i*h2(k) ) mod m

与线性探查和二次探查不同的是,这里的探查序列以两种方式依赖于关键字k。

11.5 完全散列

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

设计完全散列方案的基本想法就是采用一种两级的散列方案,每一级采用全域散列。

 

 

 

原创粉丝点击