数据结构Note1:Hash 表(散列表/哈希表)

数据结构Note1:Hash 表(散列表/哈希表)

从直接寻址表到散列表

散列表是普通数组的概念的推广,对于普通数组,可以直接寻址,而 Hash 表可以在 O(1) 的时间内访问任意位置。

对于数据结构的元素用对象来表示,其中包含一个 key ,即关键字,和卫星数据,在访问寻址排序等操作时对 key 进行,卫星数据是对象中的其他数据,随 key 一起移动。如果 key 的全域 U 比较小,那么可以直接寻址(假设没有两个元素 key 相同)。这样就可以有一个直接寻址表(direct-address table),其中的每个位置称为一个(slot)。但是直接寻址表只能用于全域U较小的时候,如果U很大,则不现实,且由于实际的key可能只占据U中的一小部分,因此会造成空间浪费,我们希望可以将表的大小压缩,而不失其查找优势。

直接寻址表
图为直接寻址表

hash表即为解决这一问题而出现的,在直接寻址下,key = k 的元素,放在槽 k 中;而在hash表中,key = k 的元素放在槽 h(k) 中。其中 h 代表哈希函数,其将全域U映射到hash表的槽位上。由于槽位比 |U| 小得多,因此出现压缩现象,即多个元素压到一个slot中,称为冲突(collision)。解决collision的方法有:

  • 链接法(chaining)
  • 开放寻址法(open addressing)

链接法是将映射到同一个slot中的元素做成链表。设元素个数 n , slot 个数为 m,定义装载因子(load factor)α=n/m ,即一个链中平均的元素数量。worst-case下,哈希表性能很差,假设h将所有的key都映射到一个slot中,那么查找哈希表相当于查找长度为n的链表,故worst-case 为 O(n) 。但是对于平均来说结果会好。

这里写图片描述
hash表的映射关系

哈希表的平均性能依赖于hash function将所有的key分散到各个slot上的均匀程度。为了讨论复杂度,做假设:

  • 简单均匀散列假设:任何给定元素等可能的散到m个slot中的任意一个,且与其他元素在什么位置无关。

该假设下计算,得出定理,即对于链接法解决冲突的哈希表,一次成功查找和不成功平均时间都为 Θ(1+α)。其中的 Θ(1)是计算哈希值和访问slot的时间。一般来说,如果散列表中的slot个数与元素个数成正比的话,即 m = O(n) ,此时alpha是常数项,因此查找哈希表的时间为O(1),如果链表双向链接,那么插入(在每个链表的头上插入)和删除在worst-case下也是常数时间,因此所有字典操作平均情况下都是O(1)。

这里写图片描述
图为双向链接的链接法解决冲突的hash表

(小编按:似乎可以认为哈希表实际上是在储存空间和访问时间之间的一个trade-off,如果不考虑时间,应该做成链表,如果不考虑空间,应该做成direct-address,这样就可以直接寻址。而链接法解决冲突的哈希表可以认为是预先对元素进行了一个分类,而分类信息与key本身有关,这样就可以通过计算hash值来得到类别,然后在该类别中进行寻址,从而降低了访问时间。一个不太贴切单略有类似的例子是,传统的中药柜对于药材的放置是按照金木水火土五行属性分别放置于不同的方位,当医师取药时,首先默想该味药材的五行属性(计算hash),然后在相应的方位寻址。由于药材的五行偏性是自然属性较为均匀,因此较为分散。从而使得查找的过程更加简便。)

散列函数

散列函数应尽可能满足简单均匀散列假设,一种好的方法得到的哈希值应独立于数据可能存在的任何模式。某些应用中可能还会要求更强的性质,比如将近似的key映射到完全不同哈希值。散列函数有以下积累,如:

1. 除法散列法

h(k) = k mod m,速度快,m常选择距离2的幂次较远的素数(因为希望设计hash函数时考虑key的所有位,如果为2的幂次则只考虑了幂次个低位数字)。

2. 乘法散列法

h(k) = floor(m(kA mod 1)),其中floor下取整,mod 1 表示取小数部分。即先用A (0

开放寻址法

开放寻址法将所有的元素都放在hash表里,所以装载因子alpha小于1,即slot数不能少于动态集合中元素数目。开放寻址法不用指针,因此可以将存指针的位置用来做slot存数据。插入和查找key在开放寻址法产生的hash表中需要用到探查(probe),插入的时候需要连续检查hash表,直到找到NIL,空slot,来存放key。查找的时候也要通过probe进行查找。

探查的顺序叫做 probe sequence,可以写为:

< h(k,0), h(k,1), … , h(k,m-1)>

这个序列是 0 到 m-1 的一个排列,每一项不仅与key值有关,还与当前的探查号。过程即:i = 0,开始计算h(k,i),如果该位置是空slot,放入,否则,i ++ ,继续计算,直到放入。查找过程类似,如果没有查到,i ++ 继续向下探查,如果到了NIL,说明没有该元素。

但是这样一来,元素的delete操作就有了困难,如果删除了某个位置的元素,将其置成NIL,则探查序列中在它之后的元素,即访问时需要经过被删除元素才能被访问到的那些元素就没法被查找了。一个解决方法就是,用DELETED标识符代替NIL,这样的话,在插入时,遇到DELETED和NIL同等对待,也可以插入;而在查找时,将DELETED视为一个被填充的slot。所以如果必须要删除的时候一般采用链接法解决collision。

均匀散列假设:是简单均匀散列的概念的一般化,指的是每个key的probe sequence等可能的是 0 到 m-1 的 m!种全排列中的一种。

常见的探查方法:(都不满足上面的假设,因为需要m!种序列,而下述方法最多m^2种)

1. 线性探查

h(k,i) = (h’(k)+i) mod m
其中h’为辅助散列函数。随着连续被占用的slot增加,会产生一次群集(primary clustering)。

2. 二次探查

h(k,i) = (h’(k) + c_1 * i + c_2 * i^2) mod m, 其中 c_1, c_2 > 0
会有轻度群集,即二次群集(secondary clustering)。

3. 双重散列

h(k,i) = (h_1(k) + i * h_2(k) ) mod m
最好的开放寻址法探查方法,以两种不同方式依赖于key,为查找整个hash表,h_2(k) 的值应该与表的大小m互素,简单方法:m取2的幂次,h_2 总产生奇数;或者m取成素数,h_2 小于m。

完全散列

如果关键词时静态的(static),即存入表中后key集合不变化,通过完全散列,可以在worst-case下用O(1)完成。用两级散列方法设计,先hash一次,找到slot,再hash一次,找到二级hash表的slot位置。为了确保二级hash表不冲突,需要让每个一级hash表中slot中的二级hash表的大小为在这个slot中的key个数的平方。适当选择第一级hash参数,可以将空间压到O(n)。

这里写图片描述
图为完全散列

reference:
算法导论,第三版,(CLRS)

星期三, 06. 十二月 2017 10:58下午

posted @ 2017-12-06 23:08  毛利小九郎  阅读(140)  评论(0编辑  收藏  举报