数据结构之二(hash)

Hash表

Hash表作为一种动态集合数据结构,一般只支持:插入、查询、删除操作;而且每个操作的时间复杂度一般控制在O(1)内。

Hash表是普通数组的一种推广。因为数组可以在直接通过下标来定位要查找的元素,时间为O(1)。因此hash表目标也是使用一些技术,以达到可以在O(1)的时间内完成操作。(严格来说时间是和装载因子a相关的)

Hash的方法。

1.        直接寻址法

前提:关键字的全域U比较小;且无重复关键字。

方法:关键字的全域为{0,1,…..,m-1}。m不是太大,或者可以不考虑内存限制;则使用一个数组T[0,m-1],每一个槽代表一个关键字,如果没有关键字对应到槽k,则T[k] = NULL。

缺点

关键字的全域不能太大;

如果实际关键字的数量和关键字的全域的数量相比,比较小时,则分配的T有些浪费空间。

2.        Hash表

为了解决直接寻址的缺点,从而产生hash表。

Hash表的空间为实际关键字的数量阶的,O(|K|);与直接寻址比缺点在于查找时间O(1)为平均时间,而直接寻址最坏情况也为O(1).

方法:hash表使用一个hash函数h,以key为参数来计算存储位置。Hash函数h为关键字全域U到存储域M的映射。一般U都比M大,以便达到降低存储空间的目的,因此映射不为单射。由此,两个key可能映射到同一个位置,这时称为发生碰撞。

3.        解决碰撞的方法

3.1.       链接法

把值相同的元素用一个链表来表示。插入在链表的头部插入。

装载因子:装载因子为hash表中每一个槽平均存放的元素个数。可以大于、小于、等于1。

简单一直散列:所有元素均匀散列到hash表的m个槽位,而且与已经散列的情况无关。

缺点:最坏情况下散列到一个槽,这时相对于链表。

时间:装载因子为a的简单一直散列,则查询(成功、失败)的平均时间为O(1+a)。

结果的计算,失败要找到尾部;成功则需要使用指示随机变量来计算。

从这个结果可知,如果hash表的槽位数和存放的元素为同阶的,即n=O(m)。从而a=n/m=O(1),则hash表的查询为O(1)的。

3.2.       开放寻址法

开放寻址是指:所有元素都存放在hash表中,每个槽位最多放一个元素。不像链接法,开放寻址没有链表。从而装载因子a不会超过1。

在插入的过程中,如果有冲突,则会继续查找,直到找到一个空的槽位为止。

缺点:开放寻址时,删除比较麻烦,因为删除后需要重新调整槽;所有开放寻址一般只支持插入和查找操作,如果需要删除,则一般使用链接法。

探测:冲突时,查找空槽的过程称为探测。

与普通hash比,探测需要探测号,探测了几次。因此比一般的hash多一个参数。

而且如果表有空槽,则一定会探测到。

因此:h(k,i)对每一个k来说,h(k,0),h(k,1),…,h(k,m-1)必须为0,1,….,m-1的一个排列。否则某个位置就不能被探测到。

探测方法

3.2.1.       线性探测

线性探测中,有一个辅助散列函数。或者称为h(k,0)。

h(k,i)=(h(k,0)+i) mod m

即第一次探测为h(k,0),如果非空,则找h(k,0)+1,直到找到空位置。

缺点:存在称为一次群集的问题,如果对于不同的k1,k2,如果h(k1,0)和h(k2,0)相同,则所有的探测顺序都相同。即随着时间的推移,连续的槽被占用的情况会增加,从而查询也会增加开销。

3.2.2.       二次探测

二次探测是使用二次函数来做探测。也有一个辅助散列函数。或者称为h(k,0)。

h(k,i)=(h(k,0)+c1*i+c2*i^2) mod m

c1,c2为辅助常数。初始探测也为h(k,0)。之后的探测不是线性的查找空槽,而是有偏移量的查找空槽偏移量为探测次数的二次函数。

缺点

和线性探测相同,也有群聚的现象。对于不同的k1,k2,如果h(k1,0)和h(k2,0)相同,则后续的探测都会相同。因此也会产生群聚,这种群聚称为二次群聚

另外,为了提高hash表的利用率,c1,c2,m的选择有一些限制。

3.2.3.       双重散列(二次hash)

双重散列使用两个辅助函数来进行散列计算。h1(k),h2(k)。

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

为了探测到整个表,h2(k)有一些限制。一种方法是取m为2的幂次,并使得h2只生成奇数。或者取m为质数,h2产生的数都比m小。

通常,双重散列的探测序列有O(m^2)种顺序,比一次和二次探测的O(m)种顺序多,从而与一致散列比较接近。

3.2.4.       开放寻址的复杂度分析

首先开放寻址的装载因此a<=1。

开放寻址一般假设为一致散列。

查询不成功:的次数期望为1/(1-a)。证明使用事件序列,且都探测失败。Hash表半满,则为2次;为90%满,则次数为10次。因此如果太满,则查询失败的次数将急剧增加。

插入元素:需要的探测次数的期望为1/(1-a)。因为插入需要做一次查找不成功的探测。

查询成功:的次数期望至多为(1/a)*ln(1/(1-a))。比如:hash表为半满的,则查询成功的期望为1.3次左右;hash表为90%满的,则期望为2.5次左右。

从此也可以看出,hash表的线性时间O(1)其实是和装载因子a相关的。这在链接法中的结果也是相同的。

4.        Hash函数

比较好的散列函数是尽可能的满足简单一直散列的条件。

4.1.       除法散列

使关键字对m取余。

选择的限制:m应该选择质数,且不要接近为2的幂次的数,或10的幂次。

不为2的幂次是因为,k对2的幂次取余相当于取k的低位,而将高位忽略了。

不为2的幂次减一(如:2^p-1)是因为,如果字符按2^p为基数来解释,则字符相同,但顺序不同的字符串可以映射到同样的键。

4.2.       乘法散列

选取固定的关键字0<A<1,hash函数为

h(k)=[m*(kAmod1)]----1式

含义为关键字乘上A后取小数部分,再与m相乘(即hash表的长度),取整即可。

乘法的优点是对m没有要求,因此一般选择m为2的幂次。

特别:

计算机一个字节为w个bit位。Hash表的表长m=2^r。则1式可表示为

h[k] = ((A*k)mod(2^w))>>(w-r)----2式

A一般取值在(2^(w-1),2^w)之间。

一般取1式中的A接近黄金分割值。

在2式中,w,r一般为事先已知(w为字节位数,r为hash表的位数),从而只需要选择A值即可将h确定下来。

2式可以理解为:将A表示为二进制的分数,k乘上A后取小数部分,并取小数部分的最高r位(即向右移动w-r位的整数)。

也可以理解为:将圆圈等分为m份,即hash表的长度;hash函数为在圆上转动k次,每次转动A个格子,最终停下来的地方即为hash函数的结果值。

1式和2式中只有乘法和对2的幂次取余,对2的幂次取余属于移位操作,因此比除法要节约时间。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值