一个hash table是用来存取一系列key
/value
对,有很多方式用来实现一个hash table
。但是都有一个共同点就是桶,每一个hash table
都会包含一系列的桶,对于每一个key
来说都会唯一的属于一个桶。为了决定某个key
是属于哪个桶,你需要对这个key
进行hash
,然后对它取模,其余数就是这个key
所属于的桶下标,整个过程如下图:
key
会属于相同的桶,对于这种情况该如何处理呢? 一个比较著名的策略就是把这些属于相同桶的key都存在一个单链表里面,这也就是所谓的拉链法,如下图:
key
都放在桶中,这种方法对缓存比较友好,如果没有找到空闲的桶存放这个key,则会利用一些策略去寻找附近的空闲的桶,而这种方法的缺点就是当这个桶满的时候,你可能查询一个key需要搜索很多桶,这取决于你的探测策略。
线性探测是最为简单的一种探测策略,加入我们插入(13,"orange")
,到hash table
中,通过hash
计算出13的hash值是0x95bb7b92
,那么我们将会按照其最低位把它存在索引是2的桶中的,但发现对应的桶中已经有元素了,那么此时在2的基础上累加,继续看索引3,以此类推。最终找到一个空闲的位置把(13,"orange")
插入进去,整个过程如下图:
聚集现象
。
二次探测较线性探测效率高,线性探测在发生冲突的时候,每次递增1,而二次探测则是按照下面这个公式来探测。
H0 = hash(x) % m;
Hi = (H0 + i^2) % m
Hi = (H0 - i^2) % m
其中i = 1,2,....(m-1)/2
第一次都一样,取hash,然后按照桶的个数取余数即可,第二次在第一次的基础上加i^2
(此时i=1),第三次的时候,在第一次的基础上减i^2
(此时i=2),以此类推。二次探测相比于线性探测来说可以避免聚集(属于相同桶的key顺序排列在一起)现象。然而二次探测又引入了另外一种聚集问题,就是所有映射到相同桶的关键字在寻找空位的时候的探测步长都是固定的。为了解决这个二次聚集
的问题引入了重hash。
二次聚集
产生的原因是二次探测算法产生的探测序列步长总是固定的,如1, 4, 9, 16等,现在需要的一种方法是产生一种依赖关键字的探测序列,而不是每个关键字都一样,那么,即使不同的关键字映射到了相同的桶,也可以使用不同的探测序列。方法就是把不同的关键字用不同的哈希函数再做一遍哈希化,用这个结果作为步长,对指定的关键字步长在整个探测中的不变的,不同的关键字使用不同的步长。 第二个哈希函数应具备的要点:1.和第一个哈希函数不同;2.不能输出0(否则,将没有步长;每次探测都是原位置,算法将陷入死循环)
Cuckoo_hashing,关于Cuckoo Hash
可以参考wiki上的介绍,这种探测策略具有占用空间小查询迅速等特性,可以用于Bloom filter,内存管理等。它的原理也很简单,类似于布谷鸟在别的鸟巢中下蛋,并将别的鸟蛋挤出的行为。算法使用两个不同哈希函数计算对应key所属于的桶。
布谷鸟hash还有很多变形,其中一种就是通过增加hash函数,进一步提高空间的利用率,还有一种则是增加哈希表,每个哈希函数对应一个哈希表,每次选择多个张表中空余桶进行放置。
Hopscotch hashing,非常适合去实现一个并发的HashMap。
首先对key进行hash得到桶的下标i
。
i
的桶是空的,则插入key
到桶中,然后返回。i
开始线性探测,直到找到一个空闲的桶,下标为j
i
在H-1
范围内,则把key插入到桶中然后返回,否则认为j
远离了i
,为了找到一个离i
近的,空闲的桶,需要找到一个桶在i
和j
之间并且距离j
在H-1
范围内,然后把j
替换成y
,这个时候y
所在的位置就空闲起来了,这个时候再查看y
是否距离i
在H-1
范围内,如果不在就继续步骤3直到找到一个符号条件的就把key插入到桶中,如果最终没有找到就进行hash table
扩容。这是作者Jeff Preshing发明的一个解决hash
冲突的方法灵感来自于Hopscotch hashing
,也是本文重点要介绍的一种算法。在Leapfrog Probing
算法中,我们需要给每个桶配置两个额外的空间,这个空间里面存的值就决定了每个桶的探测策略,如下图:
例如: 插入40到hash table中,通过hash计算得到其值为0x674a0243
,通过取模得到对应的桶是3,因此首先检查下标为3的如果不为空,则取第一个空间中的值2作为步长,再次检查下标为5的桶,依然不为空,取第二个空间中的值3,再次检查下标为8的桶,此时桶为空,查找到此结束。 整个查找过程严重依赖空间中的值,那么如何得到这些值变得很关键,而这整个过程都是在插入的时候完成。插入一个元素到hash table中要分为两步,如果key要插入的桶已经满了,并且第一个空间中的值不为0,这个时候需要使用Leapfrog Probing
进行探测,否则的话走线性探测
,找到一个空闲的桶,并把下标写到初始桶的第一个空间中,例如插入一个key为orange
,通过hash计算得到0x95bb7d92
,根据取模得到桶下标为2,但是下标为2的桶已经满了,并且第一个空间中的值是0,然后走线性探测,找到下标为11的桶是空闲的,则把key插入,并把下标为2的桶的第一个空间中的值设置为9。那么下次查找的时候直接通过第一个空间中的值就可以很快查找到了。整个过程如下图:
Leapfrog Probing
的并发处理,第一种是
insert
和
get
的并发,对于
insert
来说,要分成两步,第一步是把key存放到桶中,第二步是更新空间中的值。这两步都可以实现为原子操作,但是两者组合在一起并不是原子的,当把key存放到桶中的时候,还没有更新空间中的值此时如果有对相同key的查询请求是无法查询到的,直接返回
NullValue
,以此来处理
insert
和
get
的并发,针对多
insert
的并发处理则需要更多关注。这种情况很复杂,如果有两个key属于同一个桶并发插入的时候,当第一个key开始做线性探测寻找空闲的桶,然后更新空间中的值的时候,对于第二个key来说是不可见的,如果这个时候第二个key同事也进行探测,然后找到一个空闲的桶,随机更新空间的值,这将会导致两个key不一致,因此简单来做的话就是第二key自旋等待第一个key插入的结果可见。
本文是对文章 Leapfrog Probing的学习总结。