finish conclusions on hash function.

This commit is contained in:
Shine wOng
2019-08-06 17:44:37 +08:00
parent 52c9dfa846
commit b0aac2bd73

92
thu_dsa/hash.md Normal file
View File

@@ -0,0 +1,92 @@
Conclusions on HashTable
========================
## 散列的基本概念
> 什么是散列?为什么需要散列?
据邓公所说,散列是一种思想。与已经学过的其他数据结构相比较,向量是采用循秩访问(call by rank)的访问方式,列表是采用循位置访问(call by position)的访问方式,二叉搜索树是采用循关键码访问(call by key)的访问方式,散列与他们都不一样,是采用循值访问(call by value)的访问方式。
举个例子,你现在身处同济大学嘉定校区,四周是一片荒芒,这个时候你想回家了。沿世界上所有的街道一间一间房找过去,这是循秩访问;你记得你家是住在四川省成都市某街道多少号,然后你可以依次先到四川省,再到成都市,再到某条街道,然后找到你家,这是循关键码访问;而循值访问,则是你通常会采用的方法——你根本不用去回想我家的地址是多少,你知道它就在那里,就在家这个词刚刚出现在你的脑海中的时候。想到家乡,你想到的不是地址或者一串数字,而是一个生动的影像,包含它的环境,四周的风物,已经曾经的朋友。这就是循值访问。
可以看到,相对于其他的访问方式,循值访问是将被访问对象的数值,与它在容器中的位置之间,直接建立了一个映射关系,从而对于任何对象的基本操作(访问,插入,删除)都只需要常数$O(1)$的时间,达到了最理想的境地。这就是人类需要散列的原因,你无法不被如此的诱惑所吸引。
> 完美散列
在时间与空间性能上均达到完美的散列,称为完美散列。
也就是说对于完美散列其中的每一个值都可以唯一地映射到散列表中的一个位置既无空余亦无重复。从映射角度来看完美散列是一个单射同时也是一个满射。Bitmap就是完美散列的一个例子。
可以看出,完美散列实际中并不常见,在大多数的情形下,关键码的取值是远远大于词条的个数的,设关键码的取值为$[0, R)$, 词条的个数为$N$,则$R >> N$。设散列表的大小为$M$,此时,从定义域$[0, R)$到值域$[0, M)$的映射不可能是单射,即不可避免地会出现不同的关键码映射到散列表中的同一个位置,即所谓冲突。因此就需要合理地选择这一个映射关系,即散列函数,使冲突出现的可能性最小;同时还应该事先约定好一旦出现这种冲突,应该采取的解决访问。这两个问题将在下面重点讨论,即散列函数的设计与冲突解决方案。
## 散列函数的设计
> 散列函数的设计方案?什么是好的散列函数?
前面提到,从词条空间到地址空间的映射,即散列函数,绝对不可能是单射,冲突是一定不可能避免的,但是好的散列函数应该保证尽可能地少出现冲突。由此,可以提炼出散列函数的几个设计指标。
+ 确定性。散列函数确定的条件下,同一个关键码应该总是映射到同一个地址,这样才满足一个函数的定义。
+ 快速性。是指散列地址的计算过程要尽可能快,要能在常数时间内完成。
+ 满射。好的散列函数最好是一个满射,这样可以充分利用散列空间,尽可能地减少冲突的发生。
+ 均匀性。也是为了减少冲突的发生,关键码被映射到各个散列地址的概率应该接近于$1/M$,这样可以防止汇聚(clustering)现象的发生,即关键码只被映射到少数的几个散列地址,在局部加剧散列冲突,全局的散列空间也没有得到充分地利用。
总之,为了保证冲突尽可能地少,散列函数越是随机,越是没有规律越好。
### 几个散列函数的实例
> 除余法(division method)
除余法的整体思路非常简单,即用关键码的值对散列表的长度$M$取余,即$hash(key) = key\ mod\ M$,这样可以将关键码映射到整个散列空间上。
这里问题的关键在于散列表长度$M$的选择。考虑有一组数据,其中的关键码以固定步长$S$变化实际中的数据往往就是这种形式的而不是随机的例如for循环一般就是固定步长的数据。设第$i$个数据第一次与第$j$个数据的关键码发生冲突,即
$$
S\times i \equiv S\times j\ (mod M)
$$
即$Si$与$Sj$是同余类,所以
$$
S(j - i) \equiv 0 \ (mod M)
$$
由此可解得
$$
j - i = \frac{M}{gcd(M, S)}\ gcd for Greatest Common Divisor
$$
根据上面对散列函数设计要求的分析,我们是希望散列函数可以尽可能地减少冲突,即这里的$j - i$尽可能地大,就需要保证$M$和$S$的最大公因数要尽可能小,因此$M$要和$S$互质。但是由于散列表存储的不同数据具有不同的步长$S$值,要使$M$与所有可能的步长$S$互质,只有当$M$本身就是一个素数才可能实现。
> MAD法(Multiply-add-divide method)
上面的除余法还存在着一些问题。
首先除余法得到的散列地址依然存在一定程度的连续性即原来相邻的关键码对应的散列地址也仍然是相邻的其次在除余法中关键码较小的那些词条始终被映射到散列表的起始区段其中关键码为零的元素其散列地址总是零即是一个不动点这显然违背了散列函数应该越随机越没有规律越好的原则。MAD法正是对除余法上述问题的一个改进。
MAD法顾名思义就是对于关键码依次执行乘法、加法和模余运算
$$
hash(key) = (a \times key + b)\ mod\ M
$$
这样,只要适当选择$a, b$MAD法可以很好的克服除余法原有的连续性缺陷其中参数$a$的作用是使相邻关键码的散列地址更加分散,$b$的作用是作为一个偏移量,去掉不动点。
> 数字分析法
遵循散列函数越是随机没有规律就越好的原则引入了数字分析法即对于关键码key的特定进制展开抽取其中的几位映射到一个散列地址。
比较简单的情形就是取十进制展开中的奇数位,作为散列地址,例如
$$
hash(123456789) = 13579
$$
除此以外,还有平方取中法,即对于关键码$key$取平方,然后截取中间的几位来作为散列地址。之所以选择中间的几位,是因为中间的几位是受到了原来的关键码更多数位的影响;相对于取高位数字(只受到原关键码高位数字影响)或者低位数字(只受到原关键码低位数字影响),取中间位数综合了更多位数的影响,因此随机性、均匀性更强,更不易出现冲突。
此外,还有折叠法往复折叠法。
为了保证经过这些方法得到的值仍然落在散列空间以内,通常还都需要对散列表长度$M$在取余。
> 随机数法
既然散列函数是随机性越强越好,那一个简明的思想是直接利用生成的伪随机数来构造散列地址。这样的话,任意一个伪随机数发生器本身就是一个好的散列函数了。即
$$
hash(key) = rand(key)\ mod\ M
$$
其中,$rand(key)$是系统定义的第$key$个伪随机数。