⚠ 转载请注明出处:作者:ZobinHuang,更新日期:June 16 2022
本作品由 ZobinHuang 采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,在进行使用或分享前请查看权限要求。若发现侵权行为,会采取法律手段维护作者正当合法权益,谢谢配合。
目录
有特定需要的内容直接跳转到相关章节查看即可。
哈希建模与分析方法
链式哈希模型

性能分析内容与分析思路
分析内容
我们为什么部署 Hash 系统的很重要的原因是:
- Hash 理论上将能够占用更小的内存用于存储数据;
- Hash 理论上能够带来常数时间的搜索性能,这于直接使用搜索表进行存储相比,性能得到了很大的提升
这两点将成为我们下面讨论 Hash 系统性能的重点。其中,对于第二小点,我们下面进行分析的思路是:
- 首先我们基于一定的概率分布假设,讨论发生哈希碰撞的概率;
- 然后我们基于发生哈希碰撞的概率,可以推导得出 Hash Table 单个 Slot 下挂链表的平均长度;
- 基于 Hash Table 单个 Slot 下挂链表的平均长度,我们可以求出一次哈希搜索平均所需要的时间
分析思路

事实上,对 Hash 方法的性能进行分析,归根结底是在分析其
- Hash 系统面对的输入的所有可能性来自于 Keyspace $u$。$u$ 中各个 Key 值输入 Hash 系统的概率分布是具有随机性的,我称之为
输入的随机性 ; - 由于 Hash Function 的选取具有随机性,或者说 Hash Function 中的某些参数的概率分布具有一定的随机性。Hash 系统在部署的时候将会从一个 Hash Function 集合 $H$ 中随机抽取一个 Hash Function $h$。因此对于一个输入 Key 值,其经由 Hash Function 被投射到各个 Slot 的概率分布是具有随机性的。我称之为
Hash Function 的随机性
有的读者可能会对第二个小点存在一个疑问: 为什么需要随机抽取 Hash Function,而不是使用一个确定的 Hash Function 呢?答案可以总结为:
首先,第一个原因是: 我们必须保证 Hash 系统的随机性。在不对输入 Key 值的分布进行假设的情况下,如果我们对一个已经确定的 (deterministic) Hash Function 展开研究,我们无法给出任何的概率性的保证,这不利于算法的分析。因此在不对输入 Key 值的分布进行假设的情况下,我们必须引入 Hash 系统的随机性,方法是在 Hash Function 中的一些参数上设置随机性,并且基于它们的随机性分析 Hash 系统的性能: 一个 Hash 系统的随机性越好,说明输入的 Key 值被分布到 Hash Table 各个 Slot 的个数就更加均匀,Hash 冲突的概率就更小,完成一次基于 Hash 查找的时间复杂度就更低,我们在下面将会看到基于一定假设的 Hash 系统的查找时间复杂度是常数时间复杂度。wikipedia_univeral_hashing
另一个原因是: 既然我们需要随机性,一个最简单粗暴的办法就是直接部署一个随机函数 (Completely Hash Function),比如每来一个 Key 值,我们就抛一次 $\log_2m$-bits 的硬币,以决定将该 Key 值哈希到哪个 Slot 上去。这样做是否合理呢?实际上是不合理的,因为我们必须保证 Insert 一个 Key 的时候的哈希值和 Query 一个 Key 的时候的哈希值是相等的,这样 Hash Table 才有意义,这样就意味着我们必须存储针对各个 Key 值的哈希结果。因此,如果采用随机函数的方案,我们将花费一个很大的空间来存储各个 Key 值抛硬币的结果,这是得不偿失的。替代的方案是在不对输入 Key 值的分布进行假设的情况下,我们在 Hash Function 中设置一定的概率性参数,这些参数的概率分布使得我们可以像分析随机函数那样来分析这个 Hash 系统,尽管这些参数在部署 Hash 系统的那一刻开始就已经被确定,但是在部署之前我们仍然可以基于这些参数的概率特性来分析整个系统的性能。
在对 Hash 系统进行分析的时候,为了更加简单和合理,我们应该只保留两个随机性的其中一个。我们在下面将分别对这两个随机性进行讨论: 在 hash_performace_analyse_random_input 中看到对输入 Key 值随机性的讨论,在 hash_performace_analyse_hash_function 中看到对 Hash Function 随机性的讨论。
基于输入分布的性能分析
本小节主要参考自 mit6006hashing。

在本小节的分析中,我们将基于输入随机性进行讨论,换句话说,我们将忽略 Hash Function 的随机性,我们暂时假设 Hash Function 是一个确定的函数,转而我们对输入 Key 值的分布作出一定假设,使得基于一个已知 Hash Function 的系统满足如下性质:
- 均匀性 (Uniformity): 基于输入 Key 值的分布,从 Keyspace 中抽取出一个 Key,它经过 Hash Function 被哈希到 Hash Table 中的任意一个 Slot 的可能性是相等的 $\frac{1}{m}$,其中 $m$ 为 Hash Table 中 Slot 的个数;
- 独立性 (Independence): Keyspace 的每个 Key 值在 Hash Table 中的投射都是相互独立的,也就是说对于各个 Key 的投射都满足上述的均匀性,一个 Key 的哈希投射不会受到其他 Key 的哈希投射结果的影响
我们把上面的这两个针对输入 Key 值的分布的假设称为
哈希碰撞的概率
我们知道,基于 Key 值的分布,我们抽取一个 Key 值 $k_1$ 使其落入某个 Slot 的概率为:
Keyspace 的每个 Key 值在 Hash Table 中的投射都是相互独立的,因此,我们可以得到,基于 Key 值的分布,Keyspace 中不同的两个 Key 值 $k_1$ 和 $k_2$ 被映射入同一个 Slot 的概率为:
单 Slot 的 Hash Chain 长度期望
如果当前 Hash Table 中一共已经投射了 $n$ 个 Key,基于上述假设,这个 $n$ 个 Key 中的每个 Key 投射到各个 Slot 的概率分别为 $\frac{1}{m}$,并且各个 Key 值的投射是相互独立的。因此,对于一个 Slot 来说,它被投射的 Key 的个数的期望为:
我们把这个值称为
基于这个分析结果我们可以发现,如果 $m=\Theta(n)$,也即 Slot 个数的数量级和存在于 Hash Table 中的表项的个数的数量级相当,那么 $\alpha = \Theta(1)$,也即 Slot 上的链表的长度的期望将为常数值,也即 Hash Collision 发生的次数将为常数值。
查找操作的复杂度
对于一次查找操作,首先需要进行 $1$ 次哈希运算,然后在最坏情况下需要遍历对应 Slot 上的整条链表,因此查找操作的复杂度是:
如果我们有 $m=\Theta(n)$,也即 $\alpha = \Theta(1)$,那么查找操作的复杂度将是 $O(1+|\text{Chain}|) = O(1+\alpha) = O(1)$,也即一次哈希查找操作将花费常数时间复杂度。
基于 Hash Function 分布的性能分析
本小节主要参考自 mit6046hashing。

在 hash_performace_analyse_random_input 阐述的思路中,我们使用的思路是
基于上述的说法,我们有两种办法以哈希分布的方式来对 Hash Function 展开分析,分别是
- Universal Hashing 更加实用,因为它不对输入以及存在于哈希表中的键值有任何先验的假设,可以视为一种在线的哈希算法;
- Perfect Hashing 展示了更加漂亮的理论结果,因为它虽然不对输入进行假设,但是它假设了存在于哈希表中的键值是固定不变的,可以视为一种离线的哈希算法
下面我们分别对他们展开进行分析。
Universal Hashing
Universality
对于 Hash Function 集合 $\mathcal{H}$,我们说集合 $\mathcal{H}$ 是
基于输入分布假设得到的的
equ_input_distribution_hash_collision 是基于「对输入进行了均匀性和独立性的假设」得出来的;equ_universality 是对具有 Universality 性质的 $\mathcal{H}$ 中的任意哈希函数 $h$ 所做出的假设,并没有对输入进行任何假设,也就是说对于任意输入 $k, k' \in u$ 成立
Universality 的探究范围可以总结为下图,实际上规定的是两个不同的 Key 值在同一个 Slot 上发生碰撞的概率。

基于
证明:
假设哈希表中的 $m$ 个 Slots 已经装载了 $n$ 条键值表项。对于任意键值 $k_i$ 和 $k_j$,我们定义 indicator valuable:
则我们可以求出:
Uniform Difference Property
对于 Universal Hashing,存在一个更加严格的定义:

Pairwise Independence
Universal Hashing 更加严格的定义是
Pairwise Independence 的探究范围可以总结为下图,实际上其规定的就是

除了 Pairwise Independence,可以想象的是我们还会有
Perfect Hashing
基本介绍
和 Universal Hashing 不同,Perfect Hashing 假设映射到 Hash 表中的表项已经固定不变了,也即一种离线的哈希方法。可以理解为构建出来的哈希表仅仅有查询的功能,而不具有增删的功能。
Peferct Hashing 的目标是:
- 在很大概率下实现多项式时间的构建时间;
- 在最坏情况下,仍然实现 $O(1)$ 的搜索时间;
- 在最坏情况下,仍然实现 $O(n)$ 的存储复杂度;
构建过程
Perfect Hashing 的构建方法如下所示:
- 从上述介绍的 Universal Hash Family 中选取出一个哈希函数 $h_1: \{0, 1, ..., u-1\} \rightarrow \{0, 1, ..., m-1\}$,保证 $m=\Theta(n)$,然后将所有的 $n$ 个待映射的 key 值映射到 $h_1$ 对应的 $m$ 个哈希桶中去;
-
对于哈希函数 $h_1$ 对应的各个 Slot $j \in \{0, 1, ..., m-1\}$:
- 使用 $l_j$ 代表第 $j$ 个哈希桶中存储的表项的个数;
- 对于 Slot $j$,从 Universal Hash Family 中选取出一个哈希函数 $h_{2,j}: \{0, 1, ..., u-1\} \rightarrow \{0, 1, ..., m_j\}$,其中 $l_j^2 \le m_j \le O(l_j^2)$;
- 将第 1 步构建的 Hash with Chaining 的各个 Slot $j$ 上的链表替换为 $h_{2,j}$ 对应的哈希表;
综上,我们可以得到,整个结构占用的空间大小为:
为了保证整体结构的存储复杂度是 $O(n)$,使用 $l_j$ 代表哈希函数 $h_1$ 对应的哈希表中,第 $j$ 个哈希桶中存储的表项的个数,如果 $\sum\limits_{j=0}^{m-1}l_j^2 > cn$,则重复第 1 步 (p.s. $c$ 是一个人为设置的参数);
另外,我们可以得到整体的搜索时间为:
为了保证搜索时间的复杂度为 $O(1)$,我们必须让第二张哈希表的搜索时间同样降低为 $O(1)$,因此在第二张哈希表中,当发生 Hash Collision,也即 $h_{2,j}(k_i) = h_{2,j}(k_{i'})$ 发生时,Perfect Hashing 需要对 $h_{2,j}$ 进行重新选择,并且重新哈希 $h_1$ 对应 Slot 中记录的 Key 值,直到没有冲突产生。