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的幂次取余属于移位操作,因此比除法要节约时间。