深入论证双重散列算法以及开放地址法哈希表

深入论证双重散列算法

本文尝试记录近期实现双重散列算法的过程中,对哈希表和开放地址法的更深层次的感悟。并深入论证其中的理论依据。

哈希表很常见,已渗透到编程的各个领域。除了常见的链地址法之外,开放地址法一直是略有神秘的存在。通过研究这个相对小众算法,发现其中有一些新鲜的思想给人启迪。

哈希表的设计思想,以及其中解决问题的经验曾启发其他数据结构和算法的发展。在数据结构的发展中是一个绝对重要的结构。对区块链、摘要加密都产生了重要影响。甚至人工智能中的降维概念也和哈希表有深远的渊源。

通过研究双重散列,可以更好的理解哈希表的数学原理,以及数据结构设计上的参考。例如,处理冲突的方法、动态扩容的策略,以及对优秀哈希函数的需求,都是数据结构设计中重要的考量。

哈希表

哈希表的本质

哈希表的本质就是降维投影。

降维

一个对象实际上就是一个二维的树,对象的每个Field是一个节点,原始类型的Field是叶子节点,而对象类型的Field又是一个分支节点。

将二维的对象树降维成一个整数的过程就是哈希。好的哈希算法要求降维过程中每一个节点的细微变化将对最终的数产生极大影响。

此处放一张灵魂手绘图,便于理解:
哈希表的降维投影

投影

而将有限个哈希数,从大区间(整数区间)进入一个小的区间(数组区间),这个过程就是投影。
目的是以尽量少的数组容量放置这些哈希数。
假如有N个数,一般会选一个大于N的数M作为数组容量,让数组不要过于拥挤,减少冲突。
N / M 称为加载因子(Load Factor),常用加载因子是0.75,正好是3/4,方便计算。

取模

投影的方法借助取模这个数学工具。将任意整数 k 对 M 取模,结果一定在 [0, M-1] 之间。
h 1 ( k ) = k ( m o d M ) h1(k) = k \pmod M h1(k)=k(modM)

冲突

两个数投影到同一个位置,重叠时称为哈希冲突,解决哈希冲突的不同方式产生了两种主要流派:

  • 主流是链地址法 (Seperate Chaining),也称为链表法
  • 另一种是开放地址法 (Open Addressing),也叫做闭哈希。

链地址

链地址法是一种简单可靠的哈希表实现方法。对于哈希冲突的所有的键,以链表或树的形式形成纵深堆叠。
链表法不算复杂,而且无论是增删改查性能都极为出色,因此绝大部分哈希表都采用此法。
由于非常常见,因此不作赘述。

开放地址

开放地址法采用算法将冲突的键放在其他空位上。

假设一个哈希表的容量是M,LoadFactor是0.75,则最多存储0.75M个键。
换句话说,其中必有0.75M个位置可以存值。
假如其中有key的哈希重复,只要没有超出容量,则必然有其他空位可以存这个值。

开放地址法就是通过算法,以尽可能少的步骤,在重叠时找到空位存储重叠的键值对。

开放地址法

开放地址法是一种比较小众的算法,因为:

  • 删除、扩容等操作比较麻烦
  • 算法本身就比较难以理解,写不好容易有漏洞或死循环

因此,一般的哈希表都采用链地址法,不过一些只读哈希表会采用开放地址法。

  • 只读哈希表只需要通过Builder构造数据,一次成型,避免了删除、扩容等复杂情况
  • 开放地址法可以通过一个数组存储所有数据,不需要任何Entry对象,节省内存
  • 由于key, value可以挨在一起存储,对缓存友好,查询性能较高。

探测

从冲突的位置找空位的过程称为探测(Probe)。

线性探测

h 2 ( k , i ) = ( h 1 ( k ) + i ) ( m o d M ) h2(k,i) = (h1(k) + i) \pmod M h2(k,i)=(h1(k)+i)(modM)
其中k就是key,也就是要哈希的键。h1(k)就是原本的哈希函数 k ( m o d M ) k \pmod M k(modM)
重叠的哈希,从重叠的位置开始,依次向后一个一个位置找空位。
也就是 h1(k) + 1, h1(k) + 2, h1(k) + 3 … 以此类推
从重叠的位置开始,或者用算法算出一个比较分散的位置开始。总之最后会回到顺序查找的思路。
此法最大的问题是,顺序探测的复杂度最终会和数据规模相关,也就是最坏情况是 O(n)。

线性探测的主聚集问题

主聚集问题就是说,h1(k) 可能计算出的索引大量堆积在一起,形成了连续的块。
此时,如果发生重叠,则从重叠的位置要跨越整个堆积的块,才能找到空位。
更糟糕的是,新的空位会在原有堆积块的末尾,导致堆积块越来越大。
因此很容易发生一旦聚集就往最坏情况走的趋势。

二次探测

二次探测并不是指第二次探测! 实际上二次指的是二次方。
h 2 ( k , i ) = ( h 1 ( k ) + c 1 ∗ i + c 2 ∗ i 2 ) ( m o d M ) h2(k,i) = (h1(k) + c1*i + c2*i^2) \pmod M h2(k,i)=(h1(k)+c1i+c2i2)(modM)
二次探测是线性探测的改进法。每一轮探测非线性增长。
意思就是说 h1(k) + 二次方公式,这个公式不是线性公式,而是二次方公式。
一般直接简单的采用 i2

二次探测的次聚集问题

二次探测可以解决主聚集的问题。但是容易引发次聚集问题。

次聚集问题就是说,二次探测时,每个重叠的 h1(k) 都是一样的(因为重叠了嘛)。
而二次探测的公式也一样,因此对每一个相同的 h1(k) 二次探测的每一轮的值也一样,这样就发生了次聚集。
次聚集并非物理上连在一起,而是逻辑上聚集。

伪随机探测

h 2 ( k , i ) = ( h 1 ( k ) + p [ i ] ) ( m o d M ) h2(k,i) = (h1(k) + p[i]) \pmod M h2(k,i)=(h1(k)+p[i])(modM)
不直接用顺序索引顺序探测,而是采用伪随机数生成一个系列的数,用伪随机数序列的第i个值作为偏移来探测。
伪随机数要求对每个输入,输出应当相同,因此不是真正的随机数。如果真随机,则get的时候就无法再快速找到。
伪随机数序列可以解决主聚集,次聚集等问题。
但主要问题是伪随机数的生成比较复杂,比较耗时。

双重散列

双重散列 Double Hashing 是一种稍微复杂的算法,当出现重叠的哈希时,我们希望避免依次顺序查找空位,而是通过算法让每一轮的探测分散,尽量避免和已有数据的位置冲突。

因此我们引入第二个哈希公式,并让第二个哈希算法对每一个重复的key用不同的路径探测,以便尽量避免次聚集问题。

双重散列深入探讨

双重散列算法碰到重复的哈希键,会通过一个探测公式来轮询查找。

探测公式

探测公式一般选择: h ( k , i ) = ( h 1 ( k ) + i ∗ h 2 ( k ) ) ( m o d M ) h(k, i) = (h1(k) + i * h2(k)) \pmod M h(k,i)=(h1(k)+ih2(k))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值