散列查找性能分析
- 导读
- 一、装填因子
-
- [1.1 定义](#1.1 定义)
- [1.2 实例说明](#1.2 实例说明)
- 二、拉链法
- 三、开放定址法
- 四、性能分析
-
- [4.1 拉链法](#4.1 拉链法)
-
- [ASL: H a s h ( k e y ) = k e y m o d 11 Hash(key) = key \bmod 11 Hash(key)=keymod11](#ASL: H a s h ( k e y ) = k e y m o d 11 Hash(key) = key \bmod 11 Hash(key)=keymod11)
- [ASL: H a s h ( k e y ) = k e y m o d 5 Hash(key) = key \bmod 5 Hash(key)=keymod5](#ASL: H a s h ( k e y ) = k e y m o d 5 Hash(key) = key \bmod 5 Hash(key)=keymod5)
- 小结
- [4.2 开放地址法](#4.2 开放地址法)
- [4.3 性能优化](#4.3 性能优化)
- 结语

导读
大家好,很高兴又和大家见面啦!!!
在前面的内容中我们以及认识了 哈希表 这个 高效查找 的数据结构;
- 哈希表 通过 哈希函数 将 关键字 映射成该 关键字 所对应的 哈希地址 ,从而实现通过 关键字 直接进行访问的 高效查找
在 哈希表 中,我们可以通过以下方式来构建一个 哈希函数:
- 直接定址法
- 除留余数法
- 数字分析法
- 平方取中法
在 哈希表 中,除了 直接定址法 会完全避免 哈希冲突 外,其它的方法均会存在无法避免的 哈希冲突;
因此当遇到 哈希冲突 时,我们可以通过两类策略来解决:
- 拉链法 ,又称为 链地址法 ,通过 链表 将 同义词 链接起来,从而解决 哈希冲突
- 开放定址法 ,在发生冲突时,通过在 哈希表 中探测 空闲地址 ,并将 同义词 插入到 空闲地址 中,从而处理 哈希冲突
哈希冲突 主要受 3 3 3 个因素的影响:
- 哈希函数 :优质的 哈希函数 可以尽可能地降低冲突:
- 如,在存储一段连续的 关键字 时,采用 直接定址法 ,可以直接避免 哈希冲突
- 冲突解决策略 :当发生 冲突 时,选择合适的解决策略,能够有效降低冲突的发生
- 装填因子 :装填因子 在不同的解决策略中,对 冲突 有着不同的影响
其中 哈希函数 对 哈希冲突 的影响主要体现在 其能否将关键字均匀地映射到整个哈希表的地址空间中 ;
这里我们以 除留余数法 为例,在该构造方法中,关键字 的分布情况与 模数 相关:
- 模数 与 关键字 互质,则 关键字 能够 均匀分布
- 模数 与 关键字 有 公因数 ,则 关键字 会出现 不均匀分布
因此我们在使用 除留余数法 时,需要选择一个 不大于表长的最大质数 作为 模数 ,来实现 关键字 的 均匀分布;
那么 冲突解决策略 以及 装填因子 又是如何影响 哈希冲突 的呢?从今天的内容开始,我们会深入探讨这三者之间的关系。下面就让我们直接进入正题吧;
一、装填因子
1.1 定义
装填因子 是 衡量散列表(哈希表)空间使用情况和性能表现的一个关键指标 。一般记为 α \alpha α ,定义为一个表的 装满程度 ,即:
α = 表中记录数 n 散列表长度 m \alpha = \frac{表中记录数n}{散列表长度m} α=散列表长度m表中记录数n
哈希表 的 平均查找长度 ASL 依赖于 哈希表 的 装填因子 α \bm{\alpha} α ,而不直接依赖于 n n n 或 m m m 。直观点看:
- α \alpha α 越大,表示装填的记录 越"满" ,发生 冲突 的可能性越大
- α \alpha α 越小,表示装填的记录 越"空" ,发生 冲突 的可能性月小
1.2 实例说明
这里我们简单的说明一下 哈希表 的 满 和 空:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
就比如在上表中,此时 哈希表 中还未 装填 任何 关键字 ,因此 表中记录数 n = 0 n = 0 n=0 ,该 哈希表 的长度为 m = 11 m = 11 m=11 ,那么我们通过 n m \frac{n}{m} mn 获取的 装填因子 α = 0 11 = 0 \alpha = \frac{0}{11} = 0 α=110=0 ,此时的 哈希表 为 空表 ,因此,无论我们接下来插入的 关键字 是什么,都不会发生 冲突;
当我们通过 除留余数法 来构造 哈希函数 时,我们可以构造 H a s h ( k e y ) = k e y m o d 11 Hash(key) = key \bmod 11 Hash(key)=keymod11 这个 哈希函数 来实现 关键字 的 均匀分布;
这里我们以 关键字序列 :[19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10] 以及 线性探测法 为例,进一步说明;
当该序列中的已经有 8 8 8 个及以上的 关键字 完成装填后,如下所示:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 23 | 01 | 14 | 68 | 27 | 84 | 19 | 20 |
此时该表的 装填因子 为 α = n m = 8 11 ≈ 0.73 \alpha = \frac{n}{m} = \frac{8}{11} \approx 0.73 α=mn=118≈0.73 ,这就代表着 哈希表 已经快装 "满" 了,在这种情况下就很容易发生 冲突 ,最直观的说法就是:
- 当插入的 关键字 的余数不是 剩余 空位对应的余数时,那就一定会发生冲突
比如,这里我们选择插入一个新元素 79 79 79 ,通过 哈希函数 可知 H a s h ( k e y ) = 79 m o d 11 = 2 Hash(key) = 79 \bmod 11 = 2 Hash(key)=79mod11=2 ,通过查表可知,当前从 2 ∼ 5 2 \sim 5 2∼5 这大块的连续区域都已经 装填 了 关键字 ,因此在我们探测到 6 6 6 这个位置前,均会发生 哈希冲突 ;
若此时 关键字序列 中的所有元素均完成 装填 :
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 55 | 23 | 01 | 14 | 68 | 27 | 11 | 84 | 19 | 20 | 10 |
此时对应的 装填因子 : α = n m = 11 11 = 1 \alpha = \frac{n}{m} = \frac{11}{11} = 1 α=mn=1111=1 ,这时的 哈希表 属于 满表 的状态,现在我们无论插入任何一个新的 关键字 ,均会发生 哈希冲突;
从这个例子我们就可以简单的体会到 装填因子 对 哈希冲突 的影响;
但是不同的 冲突解决策略 中,装填因子 对 哈希冲突 的影响又不相同,接下来我们就来一起探讨一下;
二、拉链法
在 拉链法 中,当发生冲突时,我们是通过 链表 链接 同义词 的方式来解决 哈希冲突 ,这里我们还是以上面的例子为例进行说明;
对于 关键字序列 :[19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10] 当我们采用 拉链法 解决冲突时,我们便会得到下面这个 哈希表:
哈希表 0 1 2 3 4 5 6 7 8 9 10 55 11 23 01 68 14 27 84 19 20 10
此时,虽然该表的 装填因子 α = n m = 11 11 = 1 \alpha = \frac{n}{m} = \frac{11}{11} = 1 α=mn=1111=1 ,但是通过观察我们不难发现,此时若我们插入的元素对于的 哈希地址 为 4 、 6 4、6 4、6 这二者中的一个的话,仍然不会发生 哈希冲突 ;
因此 装填因子 在 拉链法 中并不能在 0 ∼ 1 0 \sim 1 0∼1 这个范围内准确的反映 哈希冲突 的概率;
那么现在问题来了,既然 拉链法 中,装填因子 无法反映 哈希冲突 的概率,那么它具体反映的是什么呢?
这里我们直接给出结论------ 装填因子 反映的是 链表的平均长度。简单的说就是:
- 当 α = 1 \alpha = 1 α=1 时,说明 哈希表 中各个 桶 中 链表 的 平均表长 为 1 1 1
因此 拉链法 中,装填因子 的值是允许 大于 1 \bm{1} 1。这里我们还是以上述的 关键字序列 为例,但是我们将 哈希函数 更改一下,如 H a s h ( k e y ) = k e y m o d 5 Hash(key) = key \bmod 5 Hash(key)=keymod5 ,此时我们得到的 哈希表 如下所示:
哈希表 0 1 2 3 4 20 55 10 01 11 27 23 68 19 14 84
此时的 装填因子 α = n m = 11 5 = 2.2 \alpha = \frac{n}{m} = \frac{11}{5} = 2.2 α=mn=511=2.2 ,从图中我们也不难发现,每个 桶 对应的 链表 的 平均表长 为 2 2 2。
因此我们说 装填因子 在 拉链法 这种 冲突处理策略 下,主要反映的是 链表的平均表长;
三、开放定址法
在 开放定址法 这种 冲突处理策略 下,在发生冲突时,我们则是通过 探测 表中的 空闲地址 的方式来处理冲突。因此一个 哈希表 能够 装填 的 关键字 个数,取决于 哈希表的长度。这样我们就不难得出一个结论:
0 m = 0 ≤ α ≤ 1 = m m \frac{0}{m} = 0 \leq \alpha \leq 1 = \frac{m}{m} m0=0≤α≤1=mm
即 装填因子 直接反映了 冲突的概率;
简单的理解就是:
- 装填因子 越接近 0 0 0 ,说明表中的 空闲地址 越多,发生 冲突 的概率越小
- 装填因子 越接近 1 1 1 ,说明表中的 空闲地址 越少,发生 冲突 的概率越大
理解了 装填因子 对不同的 冲突处理策略 的影响后,接下来我们就需要进一步探讨他们是如何影响一个 哈希表 的性能;
四、性能分析
4.1 拉链法
在 拉链法 中,由于 装填因子 反映的是 平均链表长度 ,因此我们不难得出结论:
- 装填因子 越大,哈希表 的 平均链表长度 越长
- 装填因子 越小,哈希表 的 平均链表长度 越短
而 链表 的 链表长度 又直接反映的 链表 的 查找效率:
- 链表 越长,对应的 查找效率 越低
- 链表 越短,对应的 查找效率 越高
由于 链表 的 时间复杂度 为 O ( N ) O(N) O(N) 这个数量级,那么我们不难得到不同长度的链表对应的 比较次数:
| 链表长度 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ⋯ \cdots ⋯ |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 比较次数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ⋯ \cdots ⋯ |
下面我们就通过分析 关键字序列 :[19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10] 在两种 哈希函数 下对应的 ASL;
ASL: H a s h ( k e y ) = k e y m o d 11 Hash(key) = key \bmod 11 Hash(key)=keymod11
当我们通过 哈希函数 H a s h ( k e y ) = k e y m o d 11 Hash(key) = key \bmod 11 Hash(key)=keymod11 来获取 哈希映射 时,各 关键字 在查找的过程中,其比较次数依次如下所示:
| 关键字 | 19 | 14 | 23 | 01 | 68 | 20 | 84 | 27 | 55 | 11 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 比较次数 | 1 | 1 | 1 | 2 | 1 | 1 | 1 | 1 | 1 | 2 | 1 |
其 平均查找长度 为:
A S L = ∑ i = 1 n p i c i A S L = 1 ∗ 9 + 2 ∗ 2 11 A S L = 13 11 ≈ 1.18 \begin{align*} ASL &= \sum\limits^{n}_{i = 1}p_ic_i \\ ASL &= \frac{1 * 9 + 2 * 2}{11} \\ ASL &= \frac{13}{11} \approx 1.18 \end{align*} ASLASLASL=i=1∑npici=111∗9+2∗2=1113≈1.18
ASL: H a s h ( k e y ) = k e y m o d 5 Hash(key) = key \bmod 5 Hash(key)=keymod5
当我们通过 哈希函数 H a s h ( k e y ) = k e y m o d 5 Hash(key) = key \bmod 5 Hash(key)=keymod5 来获取 哈希映射 时,各 关键字 在查找的过程中,其比较次数依次如下所示:
| 关键字 | 19 | 14 | 23 | 01 | 68 | 20 | 84 | 27 | 55 | 11 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 比较次数 | 1 | 2 | 1 | 1 | 2 | 1 | 3 | 1 | 2 | 2 | 3 |
其 平均查找长度 为:
A S L = ∑ i = 1 n p i c i A S L = 1 ∗ 5 + 2 ∗ 4 + 3 ∗ 2 11 A S L = 19 11 ≈ 1.73 \begin{align*} ASL &= \sum\limits^{n}_{i = 1}p_ic_i \\ ASL &= \frac{1 * 5 + 2 * 4 + 3 * 2}{11} \\ ASL &= \frac{19}{11} \approx 1.73 \end{align*} ASLASLASL=i=1∑npici=111∗5+2∗4+3∗2=1119≈1.73
小结
在 拉链法 中当 α \alpha α 反映的是 链表的平均长度 ,从上面的两种情况来看,我们不难得到以下结论:
- 链表平均长度 越长,查找效率越低
- 链表平均长度 越短,查找效率越高
4.2 开放地址法
在 开放定址法 中,我们有介绍了 4 4 4 种方法:
- 线性探测法 :当发生冲突时,以 固定探测步长 1 1 1 ,在表中进行 循环探测
- 平方探测法 :当发生冲突时,以 平方跳跃探测步长 ± i 2 \pm i^2 ±i2,在表中进行 循环探测
- 伪随机序列法 :当发生冲突时,以 伪随机数生成器 生成的 伪随机序列 为步长,在表中进行 循环探测
- 双散列法 :当发生冲突时,以 第二个散列函数 为 关键字 获取一个 专属探测步长 ,在表中进行 循环探测
不管是哪种方法,其基本原理都是一致的:
- 通过在 哈希表 中 探测空闲地址 来解决冲突
因此我们这里就以最简单的 线性探测法 以及 平方探测法 为例,来分析 开放定址法 的性能;
线性探测法
当我们通过 哈希函数 H a s h ( k e y ) = k e y m o d 11 Hash(key) = key \bmod 11 Hash(key)=keymod11 来获取 哈希映射 时,线性探测法 处理冲突时,对应的 哈希表 如下所示:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 55 | 23 | 01 | 14 | 68 | 27 | 11 | 84 | 19 | 20 | 10 |
各 关键字 在 插入 的过程中对应的 装填因子 以及 冲突 次数依次为:
| 关键字 | 19 | 14 | 23 | 01 | 68 | 20 | 84 | 27 | 55 | 11 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 冲突次数 | 0 | 0 | 0 | 1 | 2 | 0 | 0 | 0 | 0 | 6 | 0 |
| 装填因子 α \alpha α | 0 | 0.09 | 0.18 | 0.27 | 0.36 | 0.45 | 0.55 | 0.64 | 0.73 | 0.82 | 0.91 |
各 关键字 在查找的过程中,其比较次数依次如下所示:
| 关键字 | 19 | 14 | 23 | 01 | 68 | 20 | 84 | 27 | 55 | 11 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 比较次数 | 1 | 1 | 1 | 2 | 3 | 1 | 1 | 1 | 1 | 7 | 1 |
其 平均查找长度 为:
A S L = ∑ i = 1 n p i c i A S L = 1 ∗ 8 + 2 ∗ 1 + 3 ∗ 1 + 7 ∗ 1 11 A S L = 20 11 ≈ 1.82 \begin{align*} ASL &= \sum\limits^{n}_{i = 1}p_ic_i \\ ASL &= \frac{1 * 8 + 2 * 1 + 3 * 1 + 7 * 1}{11} \\ ASL &= \frac{20}{11} \approx 1.82 \end{align*} ASLASLASL=i=1∑npici=111∗8+2∗1+3∗1+7∗1=1120≈1.82
平方探测法
当我们通过 哈希函数 H a s h ( k e y ) = k e y m o d 11 Hash(key) = key \bmod 11 Hash(key)=keymod11 来获取 哈希映射 时,线性探测法 处理冲突时,对应的 哈希表 如下所示:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|
| 55 | 23 | 01 | 14 | 10 | 27 | 68 | 84 | 19 | 20 | 11 |
各 关键字 在 插入 的过程中对应的 装填因子 以及 冲突 次数依次为:
| 关键字 | 19 | 14 | 23 | 01 | 68 | 20 | 84 | 27 | 55 | 11 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 冲突次数 | 0 | 0 | 0 | 1 | 3 | 0 | 0 | 0 | 0 | 2 | 7 |
| 装填因子 α \alpha α | 0 | 0.09 | 0.18 | 0.27 | 0.36 | 0.45 | 0.55 | 0.64 | 0.73 | 0.82 | 0.91 |
各 关键字 在查找的过程中,其比较次数依次如下所示:
| 关键字 | 19 | 14 | 23 | 01 | 68 | 20 | 84 | 27 | 55 | 11 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 比较次数 | 1 | 1 | 1 | 2 | 4 | 1 | 1 | 1 | 1 | 3 | 8 |
其 平均查找长度 为:
A S L = ∑ i = 1 n p i c i A S L = 1 ∗ 7 + 2 ∗ 1 + 3 ∗ 1 + 4 ∗ 1 + 8 ∗ 1 11 A S L = 24 11 ≈ 2.18 \begin{align*} ASL &= \sum\limits^{n}_{i = 1}p_ic_i \\ ASL &= \frac{1 * 7 + 2 * 1 + 3 * 1 + 4 * 1 + 8 * 1}{11} \\ ASL &= \frac{24}{11} \approx 2.18 \end{align*} ASLASLASL=i=1∑npici=111∗7+2∗1+3∗1+4∗1+8∗1=1124≈2.18
小结
在 开放定址法 中 装填因子 反映的是 冲突的概率。
从上面的介绍中我们不难发现,随着 装填因子 的增大,冲突的次数 也有增加:
- 当 α ≥ 0.8 \alpha \geq 0.8 α≥0.8 时,冲突的次数会明显增加:
- 如 线性探测法 中, α = 0.82 \alpha = 0.82 α=0.82 时发生冲突,此时的冲突次数为 6 6 6
- 平方探测法 中, α = 0.91 \alpha = 0.91 α=0.91 时发生冲突,此时的冲突次数为 7 7 7
因此我们可以得到结论:
- 装填因子 越大,开放定址法 的查找效率越低
- 装填因子 越小,开放定址法 的查找效率越高
4.3 性能优化
由于 哈希表 的性能直接由 装填因子 反映,因此在 哈希函数 相同的情况下,不同的 冲突解决策略 的优化思路也不相同:
- 拉链法 中,装填因子 反映的是 平均链表长度 ,因此对 拉链法 进行优化时,主要是优化 平均链表长度
- 开放定址法 中,装填因子 反映的是 冲突的概率 ,因此对 开放定址法 进行优化时,主要是优化 冲突的概率
拉链法优化
对于 查找效率 为 O ( N ) O(N) O(N) 的 链表 而言,其优化方向为:
- 减少 链表 的长度
因此我们可以通过设置一个阈值,如当链表长度到达 L L L 时,我们通过相应的优化方案来减少 链表 的长度:
- 使用一个更大的 哈希表 ,对表中的 关键字 通过 再哈希 将其映射到 新的哈希表 中,以此来达到 减少链表长度 的目的
- 通过将 链表 的 线性结构 优化为 树形结构 ,如 AVL、RBT 使其 查找效率 从 O ( N ) O(N) O(N) 下降到 O ( log N ) O(\log N) O(logN)
开放定址法优化
对于 开放定址法 而言,由于其 哈希冲突 的概率与 装填因子 密切相关,因此我们不妨设置一个阈值,如 α = 0.7 \alpha = 0.7 α=0.7 ,当我们在 插入操作 的过程中,发现此时的 哈希表 的 装填因子 达到这个阈值时,我们就需要通过 扩容操作 ,使用一个 更大的哈希表 来降低 装填因子 ,以此来降低 哈希冲突 的发生概率;
这里的 扩容操作 也称为 再哈希法 ,即当 哈希表 的 装填因子 到达预先设定的 扩容阈值 时,通过申请一块更大的表长为 质数 的 哈希表 ,借助新表对应的 新哈希函数 ,将原表中的关键字再哈希到新表中 ,以此来达到 降低装填因子 的目的;
小结
优化哈希表的关键在于 明智地管理装填因子:
- 对于开放定址法 ,需要保守一些,保持较小的装填因子 (如 0.5 ≤ α ≤ 0.75 0.5 \leq \alpha \leq 0.75 0.5≤α≤0.75);
- 对于 拉链法 ,则可以更激进一些,允许较大的装填因子 (如 0.75 ≤ α ≤ 1.0 0.75 \leq \alpha \leq 1.0 0.75≤α≤1.0)
当 装填因子 达到相应的阈值时,通过 动态扩容 和 可能的链表树化 来维持高性能
当我们选择 动态扩容 的 在哈希法 时,实际上就是 通过空间换时间 ,以牺牲更多的空间来维持高性能的过程;
在 Java 8 的 HashMap 中则采用的是 链表树化 的方式,以保证在 最坏情况 下仍有 较好的查询效率;
结语
在今天的内容中我们介绍了 哈希表的性能 的相关知识点。哈希表 的性能与 装填因子 密切相关:
- 在相同的 哈希函数 下,使用 开放定址法 处理冲突策略时:
- 装填因子 越大,哈希表 发生 冲突 的概率越高
- 装填因子 越小,哈希表 发生 冲突 的概率越低
- 在相同的 哈希函数 下,使用 拉链法 处理冲突策略时:
- 装填因子 越大,链表 的 平均长度 越长
- 装填因子 越小,链表 的 平均长度 越短
因此,为了维持 哈希表 的 高性能 ,我们可以采用 再哈希法 这种 动态扩容 操作,以 空间换时间 的方式,来达到维持 较小的装填因子 的目的;
对于使用了 拉链法 处理冲突策略的 哈希表 ,我们还可以采用 链表树化 的方式,来维持 最坏情况下 的 查找效率;
回顾整个 哈希表 章节,我们从其 基本思想 出发,学习了多种 哈希函数构造方法 ,深入分析了 拉链法 与四种 开放定址法 这两种核心的冲突解决策略,并最终落脚于本章的 性能分析与优化 。哈希表的精髓在于通过巧妙的映射,在平均情况下实现接近 O ( 1 ) O(1) O(1) 的高效查找,其性能是 哈希函数 、冲突策略 与 装填因子 三者共同作用的结果。
掌握了 高效的查找技术 之后,我们的学习之旅将进入【数据结构与算法】的另一个核心领域------排序。
有序的数据不仅能提升查找效率 (如二分查找 ),其本身也是许多 复杂算法 和 数据处理流程 的基础。在接下来的章节中,我们将一同探索各类排序算法的奥秘,从直观的冒泡排序到高效的快速排序、归并排序,理解它们的思想、实现与性能差异。
敬请期待,我们 排序 章节再见!
互动与分享
-
点赞👍 - 您的认可是我持续创作的最大动力
-
收藏⭐ - 方便随时回顾这些重要的基础概念
-
转发↗️ - 分享给更多可能需要的朋友
-
评论💬 - 欢迎留下您的宝贵意见或想讨论的话题
感谢您的耐心阅读! 关注博主,不错过更多技术干货。我们下一篇再见!