分类目录:《算法设计与分析》总目录
相关文章:
·散列表/哈希表(Hash Table)(一):基础知识
·散列表/哈希表(Hash Table)(二):直接寻址表
·散列表/哈希表(Hash Table)(三):散列表原理
·散列表/哈希表(Hash Table)(四):散列函数
·散列表/哈希表(Hash Table)(五):开放寻址法
·散列表/哈希表(Hash Table)(六):完全散列
在开放寻址法中,所有的元素都存放在散列表里。也就是说,每个表项或包含动态集合的一个元素,或包含 N o n e None None。当查找某个元素时,要系统地检查所有的表项,直到找到所需的元素,或者最终查明该元素不在表中。不像链接法,这里既没有链表,也没有元素存放在散列表外。因此在开放寻址法中,散列表可能会被填满,以至于不能插入任何新的元素。该方法导致的一个结果便是装载因子 a a a绝对不会超过1。
开放寻址法的核心思想是,如果出现了散列冲突,我们就重新探测一个空闲位置,将其插入。比如,我们可以使用线性探测法。当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,如果遍历到尾部都没有找到空闲的位置,那么我们就再从表头开始找,直到找到为止。
当然,也可以将用作链接的链表存放在散列表未用的槽中,但开放寻址法的好处就在于它不用指针,而是计算出要存取的槽序列。于是,不用存储指针而节省的空间,使得可以用同样的空间来提供更多的槽,潜在地减少了冲突,提高了检索速度。
为了使用开放寻址法插入一个元素,需要连续地检查散列表,或称为探查,直到找到一个空槽来放置待插入的关键字为止。检查的顺序不一定是
0
,
1
,
⋯
,
m
−
1
0, 1, \cdots, m-1
0,1,⋯,m−1,而是要依赖于待插入的关键字。为了确定要探查哪些槽,我们将散列函数加以扩充,使之包含探查号以作为其第二个输入参数。这样,散列函数就变为:
h
:
U
×
{
0
,
1
,
⋯
,
m
−
1
}
→
{
0
,
1
,
⋯
,
m
−
1
}
h:U×\{0, 1, \cdots, m-1\}\rightarrow \{0, 1, \cdots, m-1\}
h:U×{0,1,⋯,m−1}→{0,1,⋯,m−1}
对每一个关键字
k
k
k,使用开放寻址法的探查序列是
<
0
,
1
,
⋯
,
m
−
1
>
<0, 1, \cdots, m-1>
<0,1,⋯,m−1>的一个排列,使得当散列表逐渐填满时,每一个表位最终都可以被考虑为用来插入新关键字的槽。
查找关键字 k k k的算法的探查序列与将 k k k插入时的算法一样。因此,查找过程中碰到一个空槽时,查找算法就(非成功地)停止,因为如果 k k k在表中,它就应该在此处,而不会在探查序列随后的位置上(之所以这样说,是假定了关键字不会从散列表中删除)。
从开放寻址法的散列表中删除操作元素比较困难。当我们从槽 i i i中删除关键字时,不能仅将 N o n e None None置于其中来标识它为空。如果这样做,就会有问题:在插入关键字 k k k时,发现槽 i i i被占用了,则就被插人到后面的位置上;此时将槽 i i i中的关键字删除后,就无法检索到关键字 k k k了。
有一个解决办法,就是在槽 i i i中置一个特定的值 D E L E T E D DELETED DELETED替代 N o n e None None来标记该槽。这样就将这样的一个槽当做空槽,使得在此仍然可以插入新的关键字。
线性探查
给定一个普通的散列函数
h
′
:
U
→
{
0
,
1
,
⋯
,
m
−
1
}
h':U\rightarrow \{0, 1, \cdots, m-1\}
h′:U→{0,1,⋯,m−1},称之为辅助,线性探查方法采用的散列函数为:
h
(
k
,
i
)
=
(
h
′
(
k
)
+
i
)
m
o
d
m
h(k ,i)=(h'(k)+i)\mod m\quad
h(k,i)=(h′(k)+i)modm
给定一个关键字
k
k
k,首先探查槽
T
[
h
′
(
k
)
]
T[h'(k)]
T[h′(k)],即由辅助散列函数所给出的槽位。再探查槽T
T
[
h
′
(
k
)
+
1
]
T[h'(k)+1]
T[h′(k)+1],依此类推,直至槽
T
[
m
−
1
]
T[m-1]
T[m−1]。然后,又绕到槽
T
[
0
]
,
T
[
1
]
,
⋯
T[0], T[1], \cdots
T[0],T[1],⋯直到最后探查到槽
T
[
h
′
(
k
)
−
1
]
T[h'(k)-1]
T[h′(k)−1]。在线性探查方法中,初始探查位置决定了整个序列,故只有
m
m
m种不同的探查序列。
线性探査方法比较容易实现,但它存在着一个问题,称为一次群集。随着连续被占用的槽不断增加,平均査找时间也随之不断增加。群集现象很容易出现,这是因为当个空槽前有 i i i个满的槽时,该空槽为下一个将被占用的概率是 i + 1 m \frac{i+1}{m} mi+1。连续被占用的槽就会变得越来越长,因而平均查找时间也会越来越大。
二次探查
二次探查采用如下形式的散列函数:
h
(
k
,
i
)
=
(
h
′
(
k
)
+
c
1
i
+
c
2
i
2
)
m
o
d
m
h(k ,i)=(h'(k)+c_1i+c_2i^2)\mod m\quad
h(k,i)=(h′(k)+c1i+c2i2)modm
其中
h
′
h'
h′是一个辅助散列函数,
c
1
c_1
c1和
c
2
c_2
c2为正的辅助常数。初始的探查位置为
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
h(k ,i)=(h_1(k)+ih_2(k))\mod m\quad
h(k,i)=(h1(k)+ih2(k))modm
其中
h
1
h_1
h1和
h
2
h_2
h2均为辅助散列函数。初始探查位置为
T
[
h
1
(
k
)
]
T[h_1(k)]
T[h1(k)],后续的探查位置是前一个位置加上偏移量
h
2
(
k
)
h_2(k)
h2(k)模
m
m
m。因此,不像线性探查或二次探查,这里的探查序列以两种不同方式依赖于关键字
k
k
k,因为初始探查位置、偏移量或者二者都可能发生变化。下图给出了一个使用双重散列法进行插入的例子。
为了能查找整个散列表,值 h 2 ( k ) h_2(k) h2(k)必须要与表的大小 m m m互质。当 m m m为素数或者2的幂时,双重散列法中用到了 Θ ( m 2 ) \Theta(m^2) Θ(m2)种探查序列,而线性探查或二次探查中用了 Θ ( m ) \Theta(m) Θ(m)种,故前者是后两种方法的种改进。因为每一对可能的 ( h 1 ( k ) , h 2 ( k ) (h_1(k), h_2(k) (h1(k),h2(k)都会产生一个不同的探查序列。因此,对于 m m m的每一种可能取值,双重散列的性能看起来就非常接近“理想的”均匀散列的性能。
尽管除素数和2的幂以外的 m m m值在理论上也能用于双重散列中,但是在实际中,要高效地产生 h 2 ( k ) h_2(k) h2(k)确保使其与 m m m互质,将变得更加困难。部分原因是这些数的相对密度中可能较小。
开放寻址散列的分析
像在链接法中的分析一样,开放寻址法的分析也是以散列表的装载因子 a = n m a=\frac{n}{m} a=mn来表达的当然,使用开放寻址法,每个槽中至多只有一个元素,因而 n ≤ m n≤m n≤m,也就意味着 a ≤ 1 a≤1 a≤1。
假设采用的是均匀散列。在这种理想的方法中,用于插入或查找每一个关键字 k k k的探查序列等可能地为 < 0 , 1 , ⋯ , m − 1 > <0, 1, \cdots, m-1> <0,1,⋯,m−1>的任意一种排列。当然,每一个给定的关键字有其相应的唯一固定的探查序列。我们这里想说的是,考虑到关键字空间上的概率分布及散列函数施于这些关键字上的操作,每一种探查序列都是等可能的。
现在就来分析在均匀散列的假设下,用开放寻址法来进行散列时探查的期望次数。则:
- 给定一个装载因子为 a = n m < 1 a=\frac{n}{m}<1 a=mn<1的开放寻址散列表,并假设是均匀散列的,则对于一次不成功的查找,其期望的探查次数至多为 1 1 − a \frac{1}{1-a} 1−a1
- 假设采用的是均匀散列,平均情况下,向一个装载因子为 a a a的开放寻址散列表中插入一个元素至多需要做 1 1 − a \frac{1}{1-a} 1−a1次探查。
- 对于一个装载因子为 a < 1 a<1 a<1的开放寻址散列表,一次成功查找中的探查期望数至多为 1 a ln 1 1 − a \frac{1}{a}\ln\frac{1}{1-a} a1ln1−a1