数据结构散列表
散列表
散列表是一个包含关键字的具有固定大小的数组,表的大小记为 tablesize 。每个关键字被映射到0到 tablesize 中的某个数,并被放到适当的单元中,这个映射称为散列函数。散列函数应尽可能地在单元之间均匀分配关键字。最后还需要解决关键字冲突的情况,即映射到同一个值。
通常需要均匀的分布关键字,需要我们选择一个适合的散列函数。
关于散列函数可以查看上一篇:常见的散列函数
散列表的负载因子为散列表中的元素个数和散列表大小的比值。
一般选择表的大小为素数,以避免一些特殊性质造成的冲突。
如果关键字为字符串,最简单的办法是把字符串中的字符的值加起来,但是这种方法在表很大时不能很好地分配关键字。另一种方法是把字符串看作有某个基数的整数,求整数的十进制值再以表的大小取模,基数取32,它包含了可能的字符个数并可以快速计算。可以在后一种方法上再做一些调整,如使用部分字符来提高速度和避免前面字符左移出结果。
更多解决冲的策略:散列冲突的解决策略
以下列举几种:
分离链接法
做法是将散列到同一个值的所有元素保存到一个链表中.
定义散列表的装填因子(load factor) λ 为散列表中的元素个数与散列表的大小的比值;
散列表的大小实际上并不重要, 而装填因子才是重要的.
分离链接散列法的一般法则是使得表的大小尽量与预料的元素个数差不多(即λ=1).
使表的大小是一个素数以保证一个好的分布, 这也是一个好的想法.
开放定址法
不同于分离链接法, 开放定址法是当冲突发生时, 就尝试选择另外一个单元, 直到找到空单元.
更正式的, 单元h0(x), h1(x), h2(x), ...依次进行试选, 其中hi(x)=(hash(x)+f(i)) mod TableSize, 且f(0) = 0. 函数f是解决冲突函数.
因为所有数据都要置入表内, 所以使用这个方案所需要的表要比分离链接散列需要的表达.
一般来说, 对不使用分离链接法的散列表来说, 其装填因子应该低于λ=0.5. 这样的表叫做探测散列表. 下面是三种常见的解决冲突的方法.
1.线性探测
在线性探测中, 函数f是i的线性函数, 一般情况下f(i) = i, 这相当于逐个探测每个单元(使用回绕)来查找出空单元.
这种方法会使一些占据的单元形成区块, 其结果成为一次聚集(primary clustering),
这种方法对于插入和不成功的查找来说大约为1/2(1 + 1 / (1 - λ)^2),而对于成功的查找来说则是1/2(1 + 1/(1-λ))
2.平方探测
平方探测就是冲突函数为二次函数的探测方法.流行的选择是f(i)=i^2.
对于这种解决办法, 如果发生冲突时, 首先考虑i=1, 此时i^2=1, 即冲突位置的下一个位置, 如果该位置为空, 则插入, 如果不为空,
则下一个探测单元在距冲突位置为i^2即2^2=4远处, 如果为空则插入, 否则继续探测距冲突位置9, 16, 25...远处
一个重要的定理:
如果使用平方探测, 且表的大小是素数,那么当表至少有一般是空的时候, 总能够插入一个新的元素
在探测散列表中标准的删除操作不能执行, 因为相应单元可能已经引起冲突,元素绕过它存储在别处. 如果删除一个, 那么可能剩下的find操作都是失败.
因此探测散列表需要懒惰删除.
虽然平方探测排除了一次聚集, 但是散列到同一位置上的那些元素将探测相同的设备单元, 这称为二次聚集(secondary clustering). 二次聚集理论上是小缺憾, 模拟结果
指出, 对每次查找, 它一般要引起另外的少于一半的探测.
3.双散列
对于双散列, 一种流行的选择是f(i)=i*hash2(x). 该公式是说, 将第二个散列函数应用到x并在距离hash2(x), 2hash2(x),......等处探测.hash2(x)选择得不好将会很糟糕.
诸如hash2(x)=R-(x mod R)这样的函数将起到良好的作用,其中R为小于TableSize的素数.
在使用双散列时, 保证表的大小为素数是非常重要的.
如果双散列正确实现, 模拟表明, 预期的探测次数几乎和随机冲突解决方法的情形相同.这使得双散列理论上很有吸引力.
不过平方探测不需要使用第二个散列函数, 从在实践中可能更简单并且快.当键为字符串时,尤其如此, 因为其散列函数计算很耗时.
再散列
当表中的元素达到一定条件后(通常为性能下降), 新建一个表, 将原表迁移过去的操作
再散列可以用平方探测以多种方法实现.
1.只要表填到一般就再散列
2.只有当插入失败时才散列, 这是一种极端的做法.
3.途中(middle-of-road)策略: 当表达到某一个装填因子时进行再散列.
由于随着装填因子的增加,标的性能的确在下降, 因此以好的截止点实现的第三种策略, 可能是最好的策略.
对于平方探测法:
建立另外一个大约两倍大的表(原表大小两倍后的第一个素数), 而且使用一个相关的新散列函数, 扫描整个原始散列表, 计算每个(未删除的)元素的新增散列值并将其插入到新表中
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出,
原文链接
如有问题, 可邮件(zxy.hope@gmail.com)咨询.