
(开放定址法)
大家好,很高兴又和大家见面啦!!! 通过前面的内容,我们已经学习了两种 开放定址法 :
但是这两种方法均会带来一定的问题:
因此,在 开放定址法 中是否存在着能够避免这种 聚集现象 的冲突处理方法呢? 答案是肯定的,这就是我们今天要介绍的 伪随机数法 以及 双散列法。 接下来就让我们一起深入探讨一下这两种方法是如何做到在处理冲突的同时还能够避免 聚集现象;
伪随机序列法 指的是人为的设置一个 伪随机序列 d_i ,如
当发生冲突时,按照 伪随机序列 去进行 循环探测 ,直到找到 空闲地址; 熟悉 C 语言的小伙伴应该都知道,在 C 的标准库中存在着一个 伪随机函数 —— rand() ,它的作用就是生成一个 伪随机数,因此我们如果想要通过 伪随机序列法 来处理冲突,我们就可以借助该函数来生成 伪随机序列;
#include <stdlib.h>
int Get_Random(){
return rand() % 100; // 生成0 ~ 99 的伪随机数
}伪随机序列法 相对于 线性探测法 以及 平方探测法 ,它通过 伪随机数生成器 生成的 伪随机探测序列 d_i ,能够有效的降低因 固定步长探测序列 导致的 一次聚集 以及 相同的探测序列 导致的 二次聚集 ,使得数据在 哈希表 中分布的更加均匀;
伪随机序列法 的局限性主要体现在两个方面:
当然,于 线性探测法 以及 平方探测法 一样,伪随机序列法 同样只能够进行 逻辑删除;
双散列法 是指使用两个 哈希函数 ,当通过第一个 哈希函数 Hash_1(key) 得到的 哈希地址 发生冲突时,再利用第二个 哈希函数 Hash_2(key) 计算该关键字的 地址增量 (或者说 探测序列)d_i。 双散列法 的 哈希函数 形式为:
在 双散列法 中,我们需要准备两个 哈希函数:
$$ \begin{align*} Hash_1(key) &= key \bmod m \ Hash_2(key) &= 1 + (key \bmod (m - 1)) \ {或} \enspace Hash_2(key) &= R - (key \bmod R) \end{align*} $$
对于第一个 哈希函数 我们可以按照前面介绍的 4 种方法进行设计:
上面展示的就是 除留余数法 ,具体的细节这里我就不再赘述,下面我们需要重点关注 第二个哈希函数 应该如何设计; 我们需要清楚 双散列法 的双散列,虽然指的是两个 哈希函数 ,但是这两个哈希函数均是作用于 同一个哈希表 :
因此我们在设计 第二个哈希函数 时,需要保证两点:
第一点我们很好理解,因为 第二个哈希函数 的作用是在遇到 哈希冲突 时,获取 地址增量 ,若此时我们获取的 地址增量 为 0 ,那就表示该 关键字 会停留在原地,一直发生 哈希冲突; 第二点是为了确保探测序列能够覆盖散列表中的所有位置。这里我们可以借用 除留余数法 中 模数 选择 质数 的原因来帮助理解:
同理,当我们选择 Hash_2(key) = 1 + (key \bmod (m - 1)) 作为 第二个哈希函数 时,实际上就是采取的 除留余数法 的思想; 而当我们选择 Hash_2(key) = R - (key \bmod R) 时,则采取的是 伪随机数 的思想,其中 R 是一个小于 哈希表 大小的 质数; 因此,这里介绍的两种设计方案,均是为了使 关键字 在发生 哈希冲突 后,还能够继续保持 均匀分布; 下面我们就以关键字序列 [19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10, 79] 为例来说明 双散列法 中的 插入 与 查找 操作的具体过程;
对于这个拥有 12 个 关键字 的 关键字序列 ,我们可以选择使用下面这两个 哈希函数:
确定了 哈希函数 ,接下来我们就来一步一步的将各个 关键字 插入到 哈希表 中:
通过 Hash_1(key) = 19 \bmod 13 = 6 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 6 ,通过查表可知,此时该地址为 空闲地址 ,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
19 |
通过 Hash_1(key) = 14 \bmod 13 = 1 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 1 ,通过查表可知,此时该地址为 空闲地址 ,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
14 | 19 |
通过 Hash_1(key) = 23 \bmod 13 = 10 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 10 ,通过查表可知,此时该地址为 空闲地址 ,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
14 | 19 | 23 |
通过 Hash_1(key) = 01 \bmod 13 = 1 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 1 ,通过查表可知,此时该地址已经存储了 key = 14 ,导致其发生了 哈希冲突,因此我们需要通过 Hash_2(key) 来获取其 地址增量 从而获得其新的地址:
$$ \begin{align*} Hash_2(key) &= 13 - (01 \bmod 13) = 12 \enspace {地址增量}\ H_1 &= (Hash_1(key) + 1 * Hash_2(key)) \bmod 13 \ H_1 &= (1 + 1 * 12) \bmod 13 \ H_1 &= 0 \enspace {不冲突} \end{align*} $$
可以看到,通过 双散列法 ,我们找到了 key = 01 的新地址 H_1 = 0 ,并且该地址为 空闲地址,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 19 | 23 |
通过 Hash_1(key) = 68 \bmod 13 = 3 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 3 ,通过查表可知,此时该地址为 空闲地址 ,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 19 | 23 |
通过 Hash_1(key) = 20 \bmod 13 = 7 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 7 ,通过查表可知,此时该地址为 空闲地址 ,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 19 | 20 | 23 |
通过 Hash_1(key) = 84 \bmod 13 = 6 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 6 ,通过查表可知,此时该地址已经存储了 key = 19 ,导致其发生了 哈希冲突,因此我们需要通过 Hash_2(key) 来获取其 地址增量 从而获得其新的地址:
$$ \begin{align*} Hash_2(key) &= 13 - (84 \bmod 13) = 7 \enspace {地址增量}\ H_1 &= (Hash_1(key) + 1 * Hash_2(key)) \bmod 13 \ H_1 &= (6 + 1 * 7) \bmod 13 \ H_1 &= 0 \enspace {冲突} \ H_2 &= (Hash_1(key) + 2 * Hash_2(key)) \bmod 13 \ H_2 &= (6 + 2 * 7) \bmod 13 \ H_2 &= 7 \enspace {冲突} \ H_3 &= (Hash_1(key) + 3 * Hash_2(key)) \bmod 13 \ H_3 &= (6 + 3 * 7) \bmod 13 \ H_3 &= 1 \enspace {冲突} \ H_4 &= (Hash_1(key) + 4 * Hash_2(key)) \bmod 13 \ H_4 &= (6 + 4 * 7) \bmod 13 \ H_4 &= 8 \enspace {不冲突} \ \end{align*} $$
可以看到,通过 双散列法 ,我们找到了 key = 84 的新地址 H_4 = 8 ,并且该地址为 空闲地址,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 19 | 20 | 84 | 23 |
通过 Hash_1(key) = 27 \bmod 13 = 1 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 1 ,通过查表可知,此时该地址已经存储了 key = 14 ,导致其发生了 哈希冲突,因此我们需要通过 Hash_2(key) 来获取其 地址增量 从而获得其新的地址:
$$ \begin{align*} Hash_2(key) &= 13 - (27 \bmod 13) = 12 \enspace {地址增量}\ H_1 &= (Hash_1(key) + 1 * Hash_2(key)) \bmod 13 \ H_1 &= (1 + 1 * 12) \bmod 13 \ H_1 &= 0 \enspace {冲突} \ H_2 &= (Hash_1(key) + 2 * Hash_2(key)) \bmod 13 \ H_2 &= (1 + 2 * 12) \bmod 13 \ H_2 &= 12 \enspace {不冲突} \ \end{align*} $$
可以看到,通过 双散列法 ,我们找到了 key = 27 的新地址 H_2 = 12 ,并且该地址为 空闲地址,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 19 | 20 | 84 | 23 | 27 |
通过 Hash_1(key) = 55 \bmod 13 = 3 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 3 ,通过查表可知,此时该地址已经存储了 key = 68 ,导致其发生了 哈希冲突,因此我们需要通过 Hash_2(key) 来获取其 地址增量 从而获得其新的地址:
$$ \begin{align*} Hash_2(key) &= 13 - (55 \bmod 13) = 10 \enspace {地址增量}\ H_1 &= (Hash_1(key) + 1 * Hash_2(key)) \bmod 13 \ H_1 &= (3 + 1 * 10) \bmod 13 \ H_1 &= 0 \enspace {冲突} \ H_2 &= (Hash_1(key) + 2 * Hash_2(key)) \bmod 13 \ H_2 &= (3 + 2 * 10) \bmod 13 \ H_2 &= 10 \enspace {冲突} \ H_3 &= (Hash_1(key) + 3 * Hash_2(key)) \bmod 13 \ H_3 &= (3 + 3 * 10) \bmod 13 \ H_3 &= 7 \enspace {冲突} \ H_4 &= (Hash_1(key) + 4 * Hash_2(key)) \bmod 13 \ H_4 &= (3 + 4 * 10) \bmod 13 \ H_4 &= 4 \enspace {不冲突} \ \end{align*} $$
可以看到,通过 双散列法 ,我们找到了 key = 55 的新地址 H_4 = 4 ,并且该地址为 空闲地址,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 55 | 19 | 20 | 84 | 23 | 55 | 27 |
通过 Hash_1(key) = 11 \bmod 13 = 11 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 11 ,通过查表可知,此时该地址为 空闲地址 ,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 55 | 19 | 20 | 84 | 23 | 11 | 27 |
通过 Hash_1(key) = 10 \bmod 13 = 10 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 10 ,通过查表可知,此时该地址已经存储了 key = 23 ,导致其发生了 哈希冲突,因此我们需要通过 Hash_2(key) 来获取其 地址增量 从而获得其新的地址:
$$ \begin{align*} Hash_2(key) &= 13 - (10 \bmod 13) = 3 \enspace {地址增量}\ H_1 &= (Hash_1(key) + 1 * Hash_2(key)) \bmod 13 \ H_1 &= (10 + 1 * 3) \bmod 13 \ H_1 &= 0 \enspace {冲突} \ H_2 &= (Hash_1(key) + 2 * Hash_2(key)) \bmod 13 \ H_2 &= (10 + 2 * 3) \bmod 13 \ H_2 &= 3 \enspace {冲突} \ H_3 &= (Hash_1(key) + 3 * Hash_2(key)) \bmod 13 \ H_3 &= (10 + 3 * 3) \bmod 13 \ H_3 &= 6 \enspace {冲突} \ H_4 &= (Hash_1(key) + 4 * Hash_2(key)) \bmod 13 \ H_4 &= (10 + 4 * 3) \bmod 13 \ H_4 &= 9 \enspace {不冲突} \ \end{align*} $$
可以看到,通过 双散列法 ,我们找到了 key = 10 的新地址 H_4 = 9 ,并且该地址为 空闲地址,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 55 | 19 | 20 | 84 | 10 | 23 | 11 | 27 |
通过 Hash_1(key) = 79 \bmod 13 = 1 可知,该 关键字 的 哈希地址 为 Hash_1(key) = 1 ,通过查表可知,此时该地址已经存储了 key = 14 ,导致其发生了 哈希冲突,因此我们需要通过 Hash_2(key) 来获取其 地址增量 从而获得其新的地址:
$$ \begin{align*} Hash_2(key) &= 13 - (79 \bmod 13) = 12 \enspace {地址增量}\ H_1 &= (Hash_1(key) + 1 * Hash_2(key)) \bmod 13 \ H_1 &= (1 + 1 * 12) \bmod 13 \ H_1 &= 0 \enspace {冲突} \ H_2 &= (Hash_1(key) + 2 * Hash_2(key)) \bmod 13 \ H_2 &= (1 + 2 * 12) \bmod 13 \ H_2 &= 12 \enspace {冲突} \ H_3 &= (Hash_1(key) + 3 * Hash_2(key)) \bmod 13 \ H_3 &= (1 + 3 * 12) \bmod 13 \ H_3 &= 11 \enspace {冲突} \ H_4 &= (Hash_1(key) + 4 * Hash_2(key)) \bmod 13 \ H_4 &= (1 + 4 * 12) \bmod 13 \ H_4 &= 10 \enspace {冲突} \ H_5 &= (Hash_1(key) + 5 * Hash_2(key)) \bmod 13 \ H_5 &= (1 + 5 * 12) \bmod 13 \ H_5 &= 9 \enspace {冲突} \ H_6 &= (Hash_1(key) + 6 * Hash_2(key)) \bmod 13 \ H_6 &= (1 + 6 * 12) \bmod 13 \ H_6 &= 8 \enspace {冲突} \ H_7 &= (Hash_1(key) + 7 * Hash_2(key)) \bmod 13 \ H_7 &= (1 + 7 * 12) \bmod 13 \ H_7 &= 7 \enspace {冲突} \ H_8 &= (Hash_1(key) + 8 * Hash_2(key)) \bmod 13 \ H_8 &= (1 + 8 * 12) \bmod 13 \ H_8 &= 6 \enspace {冲突} \ H_9 &= (Hash_1(key) + 9 * Hash_2(key)) \bmod 13 \ H_9 &= (1 + 9 * 12) \bmod 13 \ H_9 &= 5 \enspace {不冲突} \ \end{align*} $$
可以看到,通过 双散列法 ,我们找到了 key = 79 的新地址 H_9 = 5 ,并且该地址为 空闲地址,因此直接插入该 关键字:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 55 | 79 | 19 | 20 | 84 | 10 | 23 | 11 | 27 |
通过这里的实例我们可以看到,Hash_2(key) 所计算的并不是 关键字 的最终地址,而是其发生冲突后的 地址增量; 从上述的例子可以得出一个结论:
了解了 双散列法 的 插入操作 ,接下来我们就来简单说明一下其 查找操作; 与之前介绍的 线性探测法、平方探测法、伪随机序列法 一致,其查找的过程中,按查找是否成功可以分为两大类:
这里我们举一个例子,比如我们要在表中查找 key = 12 ,其具体的查找步骤如下:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
01 | 14 | 68 | 55 | 79 | 19 | 20 | 84 | 10 | 23 | 11 | 27 |
从 哈希表 中我们可以获取 Hash(12) = 27 \neq 12 ,因此我们需要通过 Hash_2(key) 进行探测,继续查找;
$$ \begin{align*} Hash_2(key) &= 13 - (12 \bmod 13) = 1 \enspace {探测步长} \ H_1 &= (Hash_1(key) + 1 * Hash_2(key)) \bmod 13 \ H_1 &= (12 + 1 * 1) \bmod 13 \ H_1 &= 0 \ Hash(0) &= 01 \neq 12 \ H_2 &= (Hash_1(key) + 2 * Hash_2(key)) \bmod 13 \ H_2 &= (12 + 2 * 1) \bmod 13 \ H_2 &= 1 \ Hash(1) &= 14 \neq 12 \ H_3 &= (Hash_1(key) + 3 * Hash_2(key)) \bmod 13 \ H_3 &= (12 + 3 * 1) \bmod 13 \ H_3 &= 2 \ Hash(2) &= {空闲地址} \ \end{align*} $$
通过 探测查找 我们发现此时我们找到的 Hash(2) 是一个 空闲地址 ,这就表明该 哈希表 中并不存在 key = 12 这个 关键字 ,因此本次 查找失败;
双散列法 的优势主要体现在其 探测序列 的独特性上:
由于 第二个散列函数 \bm{Hash_2(key)} 为不同的关键字生成不同的、固定的探测步长 。 这使得即使多个关键字的初始哈希地址相同,它们的探测路径也会迅速分岔扩散,从而最大限度地减少了聚集(Clustering)现象 。 数据在表中分布得更均匀,这在哈希表负载因子(已存储元素数与总槽位数的比值)升高时尤为重要,能保持相对稳定的操作性能。
与 拉链法 需要为每个槽位维护额外的指针空间相比,双散列法 作为 开放寻址法 的一种,所有数据都存储在基础的数组结构中,空间开销更小 。在数据记录本身较大时,这种空间优势更明显。
双散列法也并非完美,其局限性主要在于 实现 和 负载方面 的要求:
双散列法需要设计并计算 两个散列函数,这比 线性探测 或 二次探测的实现要复杂一些,计算开销也稍大 。 更重要的是,第二个 散列函数 Hash_2(key) 的设计有严格要求:
一个常见的技巧是让表大小 m 取质数,并设计 Hash_2(key) 使其结果落在 [1, m-1]范围内。
当 哈希表 的 负载因子 接近 1(即表快被填满)时,双散列法 的性能会显著下降,插入 和 查找 操作可能需要很长的 探测序列,甚至可能因为不断探测已占用的槽位而陷入循环,导致插入失败 。 因此,使用 双散列法(以及其他 开放寻址法)时,通常需要通过再散列(Rehashing) 来动态扩容,即 当负载因子超过某个阈值(如0.7 或 0.75)时,创建一个更大的新表,并将所有元素重新哈希到新表中。
在今天的内容中我们介绍了两种 开放定址法:
相比于 线性探测法 以及 平方探测法 ,这两种方法均能够有效的避免 聚集问题; 但是这两种方法都需要通过计算获取 关键字 的 探测序列 ,这就使得其实现会比 线性探测法 以及 平方探测法 更加的困难; 不知道大家有没有发现,在我们介绍的这 4 种 开放定址法 中,都存在一个相同的问题:
那么在 开放定址法 中 装填因子 对 哈希表 的性能又会有何影响呢? 在下一篇内容中,我们将会详细探讨 哈希表的性能 ,大家记得关注哦! 互动与分享
感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!