前言
很多的应用都需要一些动态集合结构,这些动态集合结构都支持INSERT, SEARCH, DELETE的字典操作。对于普通的数组进行寻址我们需要
Θ
(
1
)
\Theta(1)
Θ(1)。但是我们实际存储的关键字数比全部的关键字要小很多,我们使用hash替代普通的数组,在合理假设的情况下,我们做上述操作的时候,也只需要
Θ
(
1
)
\Theta(1)
Θ(1)。即使hash函数在最糟糕的情况下,也就是hash都碰撞到同一个槽,这样所需要
Θ
(
n
)
\Theta(n)
Θ(n)
本文首先介绍了hash table是个什么东西,以及内部实现的原理。然后讲解了hash函数,和开放寻址法来处理hash碰撞的问题。最后讲解了完全hash,一种在最坏的情况下,也能在
Θ
(
1
)
\Theta(1)
Θ(1)的时间完成search操作。文中有些证明过程就省略了。
hash是什么
我们先考虑一下map获取其中元素的方法,就是通过key值来获取的。这里的hash表也是一样的道理。如图所示,我们对输入的key值,使用设计的hash函数来进行计算,得到相应的寻址地址,然后就可以获取到key对应的value。
在讨论hash函数之前,先介绍一下hash碰撞。
假设有
k
1
,
k
2
,
.
.
.
.
.
.
,
k
n
k_1,k_2,......,k_n
k1,k2,......,kn其中
h
(
k
i
)
h(k_i)
h(ki)=
h
(
k
j
)
h(k_j)
h(kj),则就会在hash table里面寻址到同一个位置,这样就会产生hash碰撞。解决hash碰撞的方法就是将slot扩展为链表的结构,slot里面储存这指向链表的头部。这也解释了为什么,在最坏的情况下hash的时间复杂度为n
我们让n个key映射到table上,table有m个slot,在每一个key映射到每一个slot上的概率都是独立和相等的情况下,hash表的承载因子为
α
=
n
/
m
\alpha=n/m
α=n/m
因此搜索一个存在的记录,其给定的时间为
Θ
(
1
+
α
)
\Theta(1+\alpha)
Θ(1+α)。
hash函数
hash函数的好坏与否,主要是看能否把keys均匀的分布到table的solt里面。由于我们不知道keys的分布,或者keys可能在一些局部分布会比较密集。因此很难找到一种统一的hash函数的方法。
division method
h
(
k
)
=
k
m
o
d
m
h(k)=k\ mod\ m
h(k)=k mod m
m不应为
2
k
2^k
2k,一个不太接近2的整数幂的素数是一个很好的选择。
Multiplication method
h
(
k
)
=
(
A
k
m
o
d
2
w
)
r
s
h
(
w
−
r
)
h(k)=(Ak\ mod\ 2^w)rsh(w-r)
h(k)=(Ak mod 2w)rsh(w−r)
全域hash
首先需要明确的一点是对于单一的hash函数,我们总能设计相应的key集合,让每一个key都映射到一个solt里面,因此就有了随机选择hash函数的方法也就是全域hash。
我们先定义一个有限的hash函数集合
H
H
H,hash table的槽位
0
,
1
,
.
.
.
.
.
.
.
.
.
.
m
−
1
{0,1,..........m-1}
0,1,..........m−1,对于
k
,
l
∈
U
k,l\in U
k,l∈U。在独立随机分布的情况下,使得
h
(
k
)
=
h
(
l
)
h(k)=h(l)
h(k)=h(l)的hash函数有
∣
H
∣
/
m
|H|/m
∣H∣/m个。这是一个概率问题,对于k,l在随机选择一个hash函数的情况下,他们发生碰撞的概率为
1
/
m
1/m
1/m,因此想要使得k,l发生碰撞的函数个数按概率来说需要
∣
H
∣
/
m
|H|/m
∣H∣/m。
定理:Let h be a hash function chosen (uniformly) at random from a universal set H of hash function. Suppose h is used to hash n arbitrary keys into the m slots of a table T. Then, for a given key x, we have
E
[
c
o
l
l
i
s
i
o
n
w
i
t
h
x
]
=
n
/
m
E[collision\ with\ x] = n/m
E[collision with x]=n/m
上面的定律话句话说就是,从H中随机选取的一个h,使得x发生碰撞的概率不会大于
n
m
{\frac nm}
mn(证明就省略了,也是indicate function)
构造一个全域函数集
构造一个全域hash函数集有很多种方法,现在介绍一种使用点积的构造方法(dot product)。
Let m be prime, Decompose key k into r+1 digits, each with value in the set {0, 1, …, m-1}. That is , let k =
{
k
0
.
.
.
.
.
k
r
}
\{k_0.....k_r\}
{k0.....kr}, where
0
<
=
k
i
<
=
m
0<=k_i<=m
0<=ki<=m,
randomized strategy:
Pick a = {
a
0
.
.
.
a
r
a_0...a_{r}
a0...ar}, where each ai is chosen randomly from {0, 1, … , m-1}
we define
h
a
(
k
)
=
∑
i
r
(
k
i
∗
a
i
)
m
o
d
x
h_a(k)=\sum_i^r{(k_i*a_i)}mod\ x
ha(k)=∑ir(ki∗ai)mod x
所以
∣
H
∣
=
m
r
+
1
|H|=m^{r+1}
∣H∣=mr+1
要证明全域hash,即使要证明造成hash碰撞的可能性非常低,低到可以忽略不记。k分解为
{
k
0
.
.
.
k
r
}
\{k_0...k_r\}
{k0...kr}恰恰方便证明全域hash函数,
证明过程就不给出了,结论是这样的
a
0
=
(
(
−
∑
i
=
1
r
a
i
(
x
i
−
y
i
)
)
∗
(
x
0
−
y
0
)
−
1
)
m
o
d
m
a_0=((-\sum_{i=1}^r{a_i(x_i-y_i)})*(x_0-y_0)^{-1})mod\ m
a0=((−∑i=1rai(xi−yi))∗(x0−y0)−1)mod m,
要想造成一次hash碰撞需要试
m
r
m^r
mr次也就是上述说的
∣
H
∣
m
{\frac {|H|}m}
m∣H∣
开放寻址(open address)
开放寻址和链接法的差别是,它的slot里面没有存放指针,如果hash到一个slot之后,这个槽以及被占有,就使用探查probe方法(线性探查,二次探查,双重探查)进行下一个slot的检索。
至于使用怎样的一个序列来探查slot,我们将这个探查序列表示为
<
h
(
k
,
0
)
,
.
.
.
.
.
k
(
k
,
m
−
1
)
>
<h(k,0),.....k(k,m-1)>
<h(k,0),.....k(k,m−1)>是
<
0
,
1
,
.
.
.
,
m
−
1
>
<0,1,...,m-1>
<0,1,...,m−1>的一个序列。
我们来看看insert
hashInsert(T, k)
i=0
repeat
j = h(k,i)
if T[j]==nil
T[i}=k
return i
else i++
until i==m
error "hash table overflow"
0到m-1有m!种组合,因此探查序列应该是从这m!个序列中选择一个。
线性探查
h
(
k
,
i
)
=
(
h
‘
(
k
)
+
i
)
m
o
d
m
,
i
=
0
,
.
.
.
.
,
m
−
1
h(k,i)=(h^`(k)+i)mod\ m,\ i=0,....,m-1
h(k,i)=(h‘(k)+i)mod m, i=0,....,m−1 为什么mod m这样可以实现循环的探查,但是这个循环只实现一次。
二次探查
h
(
k
,
i
)
=
(
h
‘
(
k
)
+
c
1
i
+
c
2
i
2
)
m
o
d
m
h(k,i)=(h^`(k)+c_1 i+c_2 i^2)mod\ m
h(k,i)=(h‘(k)+c1i+c2i2)mod m不做评论,和线性探查差不多,都有m种可能,由初始位置决定探查序列
双重探查
h
(
k
,
i
)
=
(
h
1
(
k
)
+
i
∗
h
2
(
k
)
)
m
o
d
m
h(k,i)=(h_1(k)+i*h_2(k))mod\ m
h(k,i)=(h1(k)+i∗h2(k))mod m
这两个函数的设计有很多讲究的,会产生
m
2
m^2
m2种序列,是这三种方法中最好的
完全hash(prefect hash)
首先要明确一点就是完全hash函数是针对静态的key集合,我们可以通过尝试多组hash函数来确定最终的函数完成key到slot的映射。最后实现在
Θ
(
1
)
\Theta(1)
Θ(1)的时间和
O
(
n
)
\Omicron(n)
O(n)的空间的hash函数。
我们有定理,随机从全域hash函数中选择一个h,对于
m
=
n
2
m=n^2
m=n2个槽下映射,发生碰撞的概率不大于1\2。我们可以试几次,就可以得到一个不发生碰撞的函数函数。 但是对于n的平方的空间消耗还是太大了,因此就引入了二级hash的方式,在两级hash里面都使用全域hash函数。第一级的slot的总数为m=n,第二级就有点讲究了为
m
j
=
n
j
2
m_j=n_j^2
mj=nj2。我们可以证明其期望的消耗的空间是小于2n的。所以多试几次就好了,稳住