从原理了解Hash

Hash

它是什么?

哈希表是又称散列表,一种以 “key-value” 形式存储数据的数据结构。所谓以 “key-value” 形式存储数据,是指任意的 key 都唯一对应到内存中的某个位置。只需要输入查找的值 key,就可以快速地找到其对应的 value。可以把哈希表理解为一种高级的数组,这种数组的下标可以是很大的整数,浮点数,字符串甚至结构体。

为什么存在?

有时关键码空间的数量级可能远远大于实际问题的空间,造成了巨大的浪费,我们使用桶(bucket)直接存放或间接指向一个词条。


优缺点?

优点:
>>>空间利用率 :

问题空间N,关键码空间R,桶数组(bucket array)或散列表(hash table),容量为M,则:

N < M << R

空间 = O( N + M )=O(N)

M尽可能与N同阶,所以至少与使用关键码空间相比空间利用率大大的提高了。

>>>常数级的查找时间 :

因为哈希表遵循的是循值访问,所以查找时间只需要O(1),这是再好不过的了。

缺点:
>>>冲突:

hash(key) = key % M
冲突是无法避免的,但是我们可以尽量减少冲突,并从以下两个方向入手:

  1. 精心设计散列表及散列函数,以尽可能降低冲突的概率;
  2. 制定可行的预案,以便在发生冲突时,能够尽快予以排解。

怎么用?

>>>循值访问
>>>散列函数的设计:
  • 除余法 :hash(key) = key % M

M应当选取素数。步长为step,gcd(step,M)=G,当且仅当 G == 1时,足迹能够遍布整个散列表。又由于step不能确定,所以M应是一个素数。

  • MAD法 (multiply-add-divide) :hash(key) = ( a * key + b ) % M

除余法有两个缺陷。

一:它有不动点。无论表长M取值如何,总有hash(0) ≡ 0

二:零阶均匀。[ 0 ,R)的关键码,平均分配至M个桶;但相邻的关键码的散列地址也比相邻。

取M为素数,a > 0,b > 0,a % M != 0。hash(key) = ( a * key + b ) % M

  • 平方取中 (mid-square) :取 key^2的中间若干位,构成地址

原理:将平方运算分解为一系列的左移操作,以及若干次加法,思想类似于快速幂,如13^2=13 + (13)<<2 + (13)<<3。如果忽略进位,每个数位都是由原关键码若干次求和得到的,因此两侧的数位是由更少的原数位累积而得,而越是居中的数位是由更多的原数位累积,截取居中的若干位,可以使得原关键码各数位对最终地址的影响彼此更为接近。

  • 多项式法 :
    hash(s=x0x1x2xn1)=x0an1+x1an2++xn2a1+xn1 hash( s = x_0,x_1,x_2,···,x_{n-1} ) = x_0*a^{n-1} + x_1*a^{n-2} + ··· + x_{n-2}*a^1 + x_{n-1}
    Karp-Rabin算法:串即是数的思想吻合。

  • 更多散列函数 :数字分析法(selecting digits),折叠法(folding),位异或法(XOR),伪随机数法(#!慎用此法)法等

>>>排解冲突 :
  • 独立链(linker-list chaining / separate chaining)

每个桶存放一个指针,冲突的词条,组成列表。封闭定址(closed addressing)策略,每个桶只能存放与这个桶单元的地址相冲突的词条。

优点 :

1. 无需为每个桶预留多个槽位

2. 任意多次的冲突都可解决

3. 删除操作实现简单、统一

但是 :

1.指针需要额外空间

2. 节点需要动态申请(时间成本要比常规操作高出两个数量级)

3. 更重要的是系统缓存几乎失效!每个桶内部的查找都是沿着对应的列表顺序进行的,在此之前各节点的插入和删除顺序是随机的,对于任何一个列表而言,其中的节点在物理空间上往往不是连续分布的,无法利用有效的缓存加速查找,当散列表的长度非常大,要使用到IO时,这种问题会更加明显。

  • 开放定址(open addressing ~ closed hashing) :

散列表所占用的空间在物理上始终是与地址一致的,无需申请额外的空间。每一个词条都可以存放在任何一个桶中。为每个桶事先约定若干备用桶,它们构成一个查找链(probing sequence/chain)。

沿查找链查找,逐个转向下一桶单元,直到

命中成功,或者抵达一个空桶失败

线性探索(Linear probing) 一旦冲突,则试探后一紧邻桶单元;

[ hash(key) + 1 ] % M
[ hash(key) + 2 ] % M
[ hash(key) + 3 ] % M
...

优点:

1. 无需附加的空间

2. 查找链具有局部性,可充分利用系统缓存,有效减少I/O

但是:

1. 操作时间大于O(1)

2. 冲突增多,以往的冲突,会导致后续的的冲突clustering。

懒惰删除: 使用时需要特别注意删除,如果直接删除,后续词条将丢失—明明存在却访问不到。这时需要进行懒惰删除,对需要删除的某一词条进行标记,查找操作遇到标记转向下一个继续查找,插入操作遇到标记则直接将词条插入在此处。

  • 平方试探(Quadratic probing) :

open addressing和closed addressing都属于线性试探,而线性试探有一个问题就是试探位置间距太小,大部分的试探的位置都集中于某一个相对很小的局部。因此不妨适当的拉开各次试探的间距,平方试探就是这一思路的具体体现。

以平方数为距离,确定下一试探桶单元

[ hash(key) + 1^2 ] % M
[ hash(key) + 2^2 ] % M
[ hash(key) + 3^2 ] % M
[ hash(key) + 4^2 ] % M

优点 :

数据聚集现象有所缓解,在查找链上,各桶间距线性递增,一旦冲突,可聪明地跳离。


缺点 :

1. 若设计外存,I/O将激增。平方试探策略将在一定程度上破坏数据访问的局部性,甚至系统缓存的功能会失效,不过通常情况下这个问题还不算很严重。不失一般性,取系统缓存页面的大小为1KB,如果桶单元只记录相应的引用,大致需要4字节,每一个缓存页面都可以容纳至少256个桶单元,1KB4B=256=162\frac{1 KB}{4B}=256=16^2,也就是说要做一次额外的I/O对换,必须连续的发生16次冲突,机率其实是非常小的。

2. 可能会出现空桶。

如 { 0, 1, 2, 3, 4, 5, … }^2 % 12 = {0, 1, 4, 9}

只会涉及到其中的4个单元。没有办法找到剩下2/3的空桶。这里M选的12是一个合数,借组数论的知识不难证明,只要表长M是合数,这种情况必然发生,因为 n^2 % M 可能的取值必然少于[M/2]([]为向上取整,()为向下取整,之后不再说明)种。

将表长变为素数,如 { 0, 1, 2, 3, 4, 5, … }^2 % 11 = {0, 1, 4, 9, 5, 3}。

M若为素数: n^2 % M 可能的取值恰好会有 [M/2]种—此前,恰由查找链的前[M/2],因为一般的素数M都是奇数,所以这个比例刚刚超过50%,这是情况可能到的最坏程度。

关于这一点的正面结论是:若M是素数,但装填因子λ\lambda< 0.5,就一定能找出;否则,不一定。

反证法证明这个结论:

假设存在 0 <= a < b < [M/2] ,使得沿着查找链,第a项和第b项彼此冲突。
a2  b2M\therefore a^2 \; 和b^2 自然属于M的某一同余类,
a2  b2(modM)a^2 \; ≡ b^2 \quad (mod M)
b2    a2  =  (b+a)(ba)0(modM)b^2\;- \;a^2 \; = \; (b+a)*(b-a) ≡ 0 \quad (mod M)
然而
0  <  ba  <  b+a  <  M 0\;<\;b\,-\,a\;< \;b\,+\,a\; < \;M

1<b+a<M1< b+a < M
得到(b+a)竟然是M是一个非平凡因子,这与M是素数矛盾!

  • 双向平方试探 :

一旦发生冲突,则交替的向前向后以递增的平方数为间隔逐一试探。

[ hash(key) + 1^2 ] % M
[ hash(key) - 1^2 ] % M
[ hash(key) + 2^2 ] % M
[ hash(key) - 2^2 ] % M
[ hash(key) + 3^2 ] % M
[ hash(key) - 3^2 ] % M
...

正向和逆向的子查找链,各包含[m/2]个互异的桶,但是有些素数会让这两个序列存在除0以外公共的桶。

结论:若表长取做素数 M = 4 * K +3,必然可以保证查找链的前M项均互异。

反证法证明:

M=4*K+3


设正向试探序列的第a步与逆向试探序列的第b步冲突,

而且应当是1<= b , a <= (M/2) 

冲突即 -b^2 和 a^2 是一个同余类

-b^2 ≡ a ^2 (mod M)

设a^2 + b^2 =n
0 ≡ a^2 + b^2 = n (mod M)

所以M是n的一个素因子,

根据费马双平方定理的推论,

n不仅能被M整除,也能被M^2整除

所以M ^2 <= a^2 + b^2 

但是与b , a <= (M/2)矛盾,即不可能成立。
费马双平方定理---任一素数p可表示为一对整数的平方和,当且仅当p%4=1

费马双平方定理的推论---任一自然数n可表示为一对正数的平方和,当且仅当在其素分解中,
形如 M = 4*K+3的每一素因子均为偶数次方。

extend

>>>什么样的哈希函数才是更好的?

  1. 确定determinism : 同一关键码总是被映射至同一单元。
  2. 快速efficiency :expected-O(1)
  3. 满射surjection : 尽可能充分地覆盖整个散列空间
  4. 均匀uniformity : 关键码映射到散列表各位置的概率尽量接近(可有效避免聚集clostering现象)

>>> 关于哈希的题

哈希表的查找只需要O(1),我觉得强大的不只是哈希表,更主要是hash带给我们的这种思想,比如说Karp-Rabin算法,而且我觉得状压也是有这种思想的,这些都是一种把信息压缩的思想,充分利用信息。附上两道这两天的leetcode的打卡题,我觉得还是很不错的。

leetcode974

leetcode287

内容是对邓俊辉邓老师学堂在线上的数据结构(下)(2020春)第九章词典的总结。(邓老师的课真的太好了)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章