开放寻址法的装载因子
给定一个能存放n个元素的、具有m个槽位的哈希表T,采用开放寻址法时T的装载因子为: α = n / m , n ≤ m \alpha=n/m,n \leq m α=n/m,n≤m
开放寻址法
解决哈希表(在一些文献中又称作散列表)冲突的方法有:链接法(chaining) 和 开放寻址法(open addressing)。本文讲解开放寻址法。
在开放寻址法中,所有元素都存在哈希表里。也就是说,每个表项或包含动态集合的一个元素,或包含
N
I
L
NIL
NIL(空)。当查找某个元素时,要系统地检查所有的表项,直到找到所需的元素或者最终查明该元素不在表中。在开放寻址法中,哈希表可能被填满,以至于不能插入任何新的元素,该方法导致的一个结果便是装载因子
α
\alpha
α绝对不会超过1。
开放寻址法的好处是其不使用指针,直接使用哈希函数来算出存取的位置,这样也可以加快存取速度。
插入关键字
为了使用开放寻址法插入一个元素,需要连续地检查哈希表,或称为探查(probe),直到找到一个空槽来放置待插入的关键字为止。检查的顺序依赖待插入的关键字。对于每一个关键字 k k k,使用开放寻址法探查序列为 < h ( k , 0 ) , h ( k , 1 ) , ⋯ , h ( k , m − 1 ) > <h(k,0), h(k,1),\cdots, h(k,m-1)> <h(k,0),h(k,1),⋯,h(k,m−1)>,假设散列表T中的元素仅为关键字,关键字 k k k等同于包含关键字 k k k的元素。 T T T中的每一个槽或包含一个关键字,或包含 N I L NIL NIL(如果该槽为空)。 H A S H − I N S E R T HASH-INSERT HASH−INSERT过程以一个哈希表 T T T和一个关键字 k k k为输入,要么返回其关键字 k k k的存储槽位,要么因为散列表已满而返回出错标志。
HASH-INSERT(T, k)
i=0
repeat
j=h(k,i)
if T[j] == NIL
T[j] = k
return j
else i=i+1
until i==m
error “hash table overflow"
查找关键字
查找关键字k的算法的探查序列与将k插入时的算法一样。因此,在查找的过程中碰到一个空槽时(该位置为NIL),查找算法就停止,因为如果 k k k在表中,它就应该在此处,而不会在探查序列随后的位置上。过程HASH-SEARCH的输入为一个哈希表 T T T和一个关键字 k k k,如果槽 j j j中包含了关键字 k k k,则返回 j j j;如果 k k k不存在 T T T中,则返回 N I L NIL NIL。
HASH-SEARCH(T, k)
i=0
repeat
j=h(k,i)
if T[j] == NIL
T[j] = k
return j
else i=i+1
until i==m
return NIL
删除关键字
从开放寻址法的哈希表中删除元素比较麻烦。当我们从槽
i
i
i中删除关键字时,不能仅仅将其置为
N
I
L
NIL
NIL来标识其为空。如果这样做,就会有问题:在插入关键字
k
k
k时,发现槽
i
i
i被占用了,则
k
k
k就被插入到后面的位置上;如果此时将槽
i
i
i中的关键字删除后,就无法检索到关键字
k
k
k了。有个解决办法,就是在槽
i
i
i中置一个特殊的值
D
E
L
E
T
E
D
DELETED
DELETED替代
N
I
L
NIL
NIL来标记该槽。这样就要对过程
H
A
S
H
−
I
N
S
E
R
T
HASH-INSERT
HASH−INSERT做相应的修改,将这样的槽当做空槽,是得在此仍然可以插入新的关键字。对
H
A
S
H
−
S
E
A
R
C
H
HASH-SEARCH
HASH−SEARCH无需做改动,因为它在搜索时会绕过
D
E
L
E
T
E
D
DELETED
DELETED标识。但是查找时间就不依赖于装载因子了。
这里给出的算法参考了Knuth 6.4 Algorithm R(TAOCP),该算法对应使用线性探测来确定关键字
k
k
k插入位置的HASH-INSERT过程的关键字k删除算法。
H
A
S
H
−
L
I
N
E
A
R
−
P
R
O
B
E
−
D
E
L
E
T
E
HASH-LINEAR-PROBE-DELETE
HASH−LINEAR−PROBE−DELETE过程以一个哈希表
T
T
T和一个关键字
k
k
k为输入。该过程没有使用删除标记
D
E
L
E
T
E
D
DELETED
DELETED这种方式,而是将关键字
k
k
k删除后,将该位置之后的关键字做移动这种方式(即重新调整各个关键字的顺序)。
$(i+1) %m $
nextIndex(i,m)
return (i + 1 <= m + 1) ? (i+1) : 1
使用线性探测确定关键字 k k k的索引
HASH-LINEAR-PROBE(T, k)
for i=0 to m-1
index = (h(k) + i) mod m
if T[index] == k or T[index] == NIL
return index
return NIL
HASH-LINEAR-PROBE-DELETE(T, k)
i = HASH-SEARCH(T,k)
if T[i] == NIL
return
T[i] = NIL
j = i
for i = i+1 to nextIndex(i,m)
if T[i] == NIL
return
r = HASH-LINEAR-PROBE(T, T[i])
if r == NIL //无法探测到T[i]应该存放的位置
return
if i <= r < j or r<j<i or j<i<=r //r循环地位于i和j之间
continue
if r != NIL and r != i //T[i]存放的位置不应该是当前位置
T[j] = T[i]
T[i] = NIL
j = i
i = i+1
开放寻址法探查序列的计算方法
有三种技术常用来计算开放寻址法中的探测序列:线性探查、二次探查和双重探查。这几种技术都能保证对每个关键字 k , < h ( k , 0 ) , h ( k , 1 ) , ⋯ , h ( k , m − 1 ) > k,<h(k,0), h(k,1),\cdots, h(k,m-1)> k,<h(k,0),h(k,1),⋯,h(k,m−1)>都是 < 0 , 1 , ⋯ , m − 1 > <0, 1,\cdots, m-1> <0,1,⋯,m−1>的一个排列。但是,这些技术都不能满足均匀哈希的假设,因为它们能产生的不同探查序列数都不超过 m 2 m^2 m2 个(均匀散列要求有 m ! m! m! 个探查序列)。在三种技术中,双重探查产生的探查序列最多,似乎能给出最好的效果。
- 线性探查(linear probing)
给定一个普通的哈希函数 h ′ : U → 0 , 1 , ⋯ , m − 1 h':U\to{0,1,\cdots,m-1} h′:U→0,1,⋯,m−1,称之为辅助哈希函数,线性探查方法采用的哈希函数为:
h ( k , i ) = ( h ′ ( k ) + i ) m o d m , i = 0 , 1 , ⋯ , m − 1 h(k,i) = (h'(k) + i) \; mod \;m,\; i=0,1,\cdots,m-1 h(k,i)=(h′(k)+i)modm,i=0,1,⋯,m−1
给定一个关键字 k k k,首先探查 T [ h ′ ( k ) ] T[h'(k)] T[h′(k)](此时的 i = 0 i=0 i=0),即由辅助哈希函数所给出的槽位。再探查 T [ h ′ ( k ) + 1 ] T[h'(k)+1] T[h′(k)+1],以此类推,直至槽 T [ m − 1 ] T[m-1] T[m−1]。然后又绕到槽 T [ 0 ] T[0] T[0], T [ 1 ] T[1] T[1], ⋯ \cdots ⋯,直到最后探查到槽 T [ h ′ ( k ) − 1 ] T[h'(k)-1] T[h′(k)−1]。在线性探查方法中,初始探查位置绝对了整个序列,故只有 m m m种不同的探查序列。
线性探查法比较容易实现,但它存在着一个问题,称为一次群集。随着连续被占用的槽不断增加,平均查找时间也随之不断增加。群集现象很容易出现,这是因为当一个空槽前有 i i i个满的槽时,该空槽为下一个将被占用的概率是 ( i + 1 ) / m (i+1)/m (i+1)/m。连续被占用的槽就会变得越来越长因而平均查找时间也会越来越大。 - 二次探查
二次探查采用如下形式的哈希函数:
h ( k , i ) = ( h ′ ( k ) + c 1 i + c 2 i 2 ) m o d m , i = 0 , 1 , ⋯ , m − 1 h(k,i) = (h'(k) + c_1i + c_2i^2) \; mod \;m,\; i=0,1,\cdots,m-1 h(k,i)=(h′(k)+c1i+c2i2)modm,i=0,1,⋯,m−1
其中 h ′ h' h′是一个辅助哈希函数, c 1 c_1 c1和 c 2 c_2 c2为正的辅助常数, i = 0 , 1 , ⋯ , m − 1 i=0,1,\cdots,m-1 i=0,1,⋯,m−1。初始的探查位置为 T [ h ′ ( k ) ] T[h'(k)] T[h′(k)],后续的探查位置要加上一个偏移量,该偏移量是一个关于 i i i的二次函数。这种探查方法的效果比线性探查好得多,但是,为了能够充分利用哈希表, c 1 c_1 c1、 c 2 c_2 c2和 m m m的值要受到限制。此外,如果两个关键字的初始探查位置相同,那么它们的探查序列也是相同的,这是因为 h ( k 1 , 0 ) = h ( k 2 , 0 ) h(k_1,0)=h(k_2,0) h(k1,0)=h(k2,0)蕴含着 h ( k 1 , i ) = h ( k 2 , i ) h(k_1,i)=h(k_2,i) h(k1,i)=h(k2,i)。像线性探查一样,初始探测位置决定了整个序列,这样也仅有 m m m个不同的探查序列。 - 双重哈希
双重哈希是用于开放寻址法的最好方法之一,因为它所产生的排列具有随机选择排列的许多特性。双重哈希采用如下的哈希函数:
h ( k , i ) = ( h 1 ( k ) + i h 2 ( k ) ) m o d m , i = 0 , 1 , ⋯ , m − 1 h(k,i) = (h_1(k) + ih_2(k)) \; mod \;m,\; i=0,1,\cdots,m-1 h(k,i)=(h1(k)+ih2(k))modm,i=0,1,⋯,m−1
其中 h 1 h_1 h1和 h 2 h_2 h2均为辅助哈希函数。初始探查位置为 T [ h 1 ( k ) ] T[h_1(k)] T[h1(k)],后续的探查位置加上偏移量 h 2 h_2 h2。因此,不像线程探查或者二次探查,这里的探查序列以两种不同方式依赖于关键字 k k k,因为初始探查位置、偏移量或者二者都可能发生变化。
为了能查找整个哈希表,值 h 2 ( k ) h_2(k) h2(k)必须要与表的大小 m m m互素(习题11.4-4)。有一种简便的方法确保这个条件成立,就是 m m m为2的幂,并设计一个总产生奇数的 h 2 h_2 h2。另一种方法是取m为素数,并设计一个总是返回较 m m m小的正整数的函数 h 2 h_2 h2。
一个结论:如果对某个关键字 k k k, m m m和 h 2 ( k ) h_2(k) h2(k)有最大公约数 d ≥ 1 d\geq1 d≥1,则在对关键字 k k k的一次不成功查找中,在返回槽 h 1 ( k ) h_1(k) h1(k)之前,要检查散列表中 1 / d 1/d 1/d个元素。于是,当 d = 1 d=1 d=1时, m m m和 h 2 ( k ) h_2(k) h2(k)互素,查找操作可能要检查整个哈希表。