Lecture 8: Hashing

冷知识:英文单词 'hash' 的本身含义为 "把...切成小块",该词来源于法语中的 'hacher',意思相似。而 'hatcher' 来源于古法语 'hache',意为斧头。

本节课主要分为 3 个部分:

  1. 复习 6.006 课程中的概念:dictionaries, chaining, simple uniform
  2. Universal hashing
  3. Perfect hashing

1. Review

Dictionary 是一种抽象数据结构 (ADT),维护着一个集合,我们称集合中的元素为 item,集合中的元素由键值对 (key, value) 构成,它支持以下操作:

  • insert(item):将 item 添加到集合中
  • delete(item):从集合中删除 item
  • search(key):在集合中寻找 key 对应的 item,如果存在就返回

Dictionary 不维护数据的顺序关系。

Hashing from 6.006

在 6.006 中介绍的 Dictionary 实现的时空复杂度如下:

  • 时间复杂度:insertdeletesearch 都是
  • 空间复杂度:

具体来说,假设:

  • 表示 key 的可能取值总量
  • 表示在 hash table 中的 keys/items 总量
  • 表示在 hash table 中的坑位 (slots) 总量

我们可以通过 hashing with chaining + table doubling 来实现 的各操作事件复杂度,其中 为 load factor。在推导过程中,我们做了一个很强的假设:我们使用的 hash function 满足 simple uniform hashing assumption (SUHA),即: 即 hash function 的散列过程是随机的,但我们知道 hash function 必须是稳定的,即输入相同时输出相同。随机性和稳定性是对立的,本节课的目的就是去除 SUHA,来看我们如何在理论上达到相同的时空复杂度。

2. Universal Hashing

理解 universal hashing 的直觉就是:将随机性加在 hash functions 的选择上。即从一个 hash functions 集合 中随机选择一个 (记为 ),其中 是一个 universal hashing family,后者满足: 即从 中随机选一个 hash function,在该情况下两个不同元素被散列到同一个位置上的概率小于 。到此你可能像我一样有疑问,这样随机选取 hash function 不就又丧失了稳定性吗?如果每次散列都要从 中随机取一个 hash function,那如何保证第二次遇到相同的元素还会被散列到相同的位置?实际上,对于同一个 hash table,随机选取 hash function 的过程只会执行一次,即对于这张 table 所有 keys 都使用同一个的 ,直到 hash table handler 因某种条件触发 rehash 时,如 table doubling,才会重新选择 hash function。而 rehash 需要将当前所有 keys/items 取出,重新散列,详情见这里的讨论

接下来,我们要先证明 universal hashing 能取得与 SUHA 相同的时空复杂度,然后再找到这样的 hashing family。

定理:从 中随机取任意不同的 个 key,且为每个 key 随机从某 universal hashing family 中取一个 hash function,记为 ,被散列到某一个 slot 中的 keys/items 数量预期小于 ,即:

当我们要证明某事件发生次数与某阈值的关系时,一种常用技巧就是 indicator variable。假设所有在 hash table 中的 keys 分别为 ,我们设 indicator variable: 那么: 因此: 以上证明了 universal hashing 能够取得与 SUHA 相同的时间复杂度。接下来举几个 universal hashing family 的例子:

Example #1: all hash functions

将所有可能的 hash functions 组成一个集合: 显然, 本身是 universal hashing family,因为对于任意一个 key 来说,从所有可能的 hash functions 中随机取一个,该 key 被散列到某个 slot 上的概率为 。但 只存在理论价值,不具备实践意义:

  1. 存储某个 需要花费 bits。而且为了存储这个 hash function,你需要使用一个 hash table 😂,这就来到了鸡生蛋还是蛋生鸡的问题
  2. 生成某个 的时间复杂度为 ,因为你需要为 个值生成对应的散列位置

Example #2: dot-product hash family

假设:

  • 是一个素数
  • ,其中 是一个整数

在实践中,你总能够将 向上取到满足上述条件的数。现在我们将所有的 keys 转化成 进制的数,即 ,取某个常数 ,定义 hash function: 于是得到 dot-product hash family

存储任意一个 只需要存储一个 key,即 本身。在 word RAM model 计算模型下,简单的数据 (如 keys) 都可以放入一个 machine word 中,这个计算模型的硬件实现能保证针对 machine words 的操作能在 的复杂度下完成,因此计算 的时间复杂度为

定理:dot-product hash family 是 universal hashing family

证明:对于任意两个不同的 keys,。它们在 进制下至少有某个位 () 不同,假设 ,我们可以推导: 其中:

  • :由于 是素数, 中存在模乘逆元 (modular multiplicative inverse)
  • :由于 进制下 区间内的随机数,因此可以认为 的任意位都是 区间内的随机数,且任意位之间的取值相互独立,于是我们可以将 位的概率和 位的概率分开考虑。同时 只与 以及所有 取值相关,与 无关,其取值为 区间内的某个值。
  • :由于 进制下 区间内的随机数, 区间内的某个值的概率一定为
  • :一个常数的期望就是它本身

至此,我们证明了 dot-product hash family 是 universal hashing family。

Example #3

CLRS 中还介绍了另一个 universal hash family:multiplicative hashing。首先选择一个素数 ,定义 ,则 是一个 universal hashing famliy。

multiplicative hashing 的直觉与轮盘赌 (Roulette Wheel) 的过程类似:

如果轮盘转动很多圈,那么最终小球落入的地方将变得不可预测,即随机。通常 是比较大的整数, 则是一个更大的数。 表示转动的总距离, 表示转动的总圈数; 表示最后一圈转动的角度。

证明:对于任意两个不同的 keys,,如果发生 collision,即 ,那么存在某个整数 ,使得: 由于 ,那么 存在模乘逆元,推导得到: 由于 ,等式左边有 种取值;对于固定的 ,等式右边最多存在 种非 0 取值,于是有: 至此,我们证明了 multiplicative hashing family 是 universal hashing family。

3. Perfect Hashing

如果已知要插入 hash table 的所有 keys,数量不变,且该 hash table 只需要支持 search(key),我们能做得更好吗?这便是 static dictionary problem

利用 universal hashing family,我们能做到平均情况下 (average case) ,时间复杂度 ,空间复杂度 ,但本节要介绍的 perfect hashing 能做到最坏情况下 (worst case),时间复杂度 ,空间复杂度 ,但整个 hash table 构建的时间复杂度为 polynomial (w.h.p),即

理解 perfect hashing 的直觉就是:使用 2-level hashing,即将 hashing with chaining 中的 chain (链表) 替换成另一个 hash table,且保证后者中不出现 hash collision,同时保证空间复杂度不变。

算法如下:

Step 1:从一个 universal hash family 中随机选取一个 hash function ,取 ,即 为最接近 的素数,利用 将所有 keys/items 散列到不同的 slots 中,用链表串联。

Step 2:针对任意 slot ,记散列到 slot 中的 keys/items 总数为 ,即 。继续从 universal hash family 中随机选取一个 hash function ,其中 ,即取 为大于或等于 最近的素数,将 slot 中的链表替换成 的 hash table。

如下图所示:

此时算法的空间复杂度为 ,为了使其减小到 ,我们还需要两个步骤:

Step 1.5:如果 为某设定好的常数,则重新执行 Step 1

Step 2.5:当某 slot 中出现 ,其中 ,则重新随机选取

上述两个步骤保证在第二层 hash table 中不存在 hash collision,且空间复杂度为 ,因此 search 在最坏情况下的时间复杂度为 。现在我们分析一下两层 hash table 的构建时间复杂度,Step 1Step 2 的空间复杂度都是 ,对于 Step 2.5 来说: 其中:

  • (1): 在 slot 中出现 hash collision 的概率小于或等于任意 组合出现 hash collision 的概率总和。若不同组合出现 hash collision 的事件相互独立,则取等号。
    1. (2):所有可能组合可能总数为 ,而每个组合下, 出现 hash collision 的概率为 ,即

以上计算过程与 birthday paradox 十分类似,有兴趣可阅读参考文献了解详情。其基本结论就是:假设一年有 天,那么如果想让 个人中存在 2 个人生日相同的概率为 ,则

综上所述,Step 2.5 执行一次出现 hash collision 的概率小于 ,那么这个过程就与问题 ”抛硬币直到抛到一次正面为止” 同构,在 Lecutre 7 中,我们计算过 ,且在极大概率下 (w.h.p),。另外在 CLRS 的课后题 Problem 11-2 中证明了 ,于是整个构建时间复杂度在极大概率下 (w.h.p) 为 ,即上文提到的 polynomial (w.h.p)。

针对 Step 1.5,定义 indicator variable: 那么就可以计算第二层 hash table 总共占用的空间: 利用 Markov inequality,要使得: 𝕡 则需要: 那么只要取得一个足够大的常数 就能满足。现在又回到了问题 ”抛硬币直到抛到一次正面为止” ,同样根据 Lecture 7, ,且在极大概率下 (w.h.p),。因此 Step 1Step 1.5 的时间复杂度在极大概率下 (w.h.p) 为

TODO

  • 阅读 perfect hashing paper。

参考资料