Dictionary Problem & Implementation

Dictionary Problem

dictionary, map, associated array 以及 symbol table 实际上都是指同一种抽象数据类型(ADT),它维护着一个键值对(这里记为 item)集合,支持以下方法:

几乎所有语言都有 Dictionary ADT 的实现,以 Python 为例:

  • Python dictionary

  • Python set (dictionary where items are keys without values)

场景

  • 几乎所有现代编程语言必备:Python, Perl, Ruby, JavaScript, Java, C++, C#, ...

  • database

  • compilers & interpreters: names -> variables

  • network routers: IP address -> wire

  • network server: port number -> socket/app

  • virtual memory: virtual address -> physical

  • substring search

  • string commonalities

  • file/directory synchronization

  • cryptography

Implementation

Associative List/Linked List

用 Linked List 将所有 item 串联起来,每次搜索最坏情况都需要遍历 Linked List,总结如下:

在 item 数量较少的时候,Linked List 的实现能展现出较高的性能,且它的常数系数很小。

Direct Access Table

将 items 存储在 array 中,用 key 做 array 的 index,但这会产生两个问题:

  1. keys 必须是非负整数 解决方案:"pre-hash" keys to integers (注意理解这里的 pre-hash)

  2. 如果 key 的范围很大,将造成空间浪费 解决方案:"hashing"

Hashing

  1. chaining:hash function 指定 item 插入的位置,插入位置相同的 key 用链表串联起来

  2. open addressing:hash function 指定 item 插入位置的顺序(多个位置,即所有位置的排列),发生冲突时,选择下一个位置

Hash Functions For Chaining

在 chaining 中,Hash Functions 指定 item 插入的位置

理想方案: Simple Uniform Hashing

什么样的 hash function 才是个好 hash function 呢?

  • 每个 key 被 hash 到每个 slot 的概率相等

  • 每个 key 被 hash 到某个 slot 的概率相互独立

在实践中,通常很难满足 Simple Uniform Hashing 的假设,但我们仍然能够做到很好的结果。基本的想法就是:避免 hash 值与 keys 出现的模式(patterns)存在相关关系。

Division Method

Multiplication Method

分为三步:

  1. 确定一个 0 到 1 之间的常数 A

  2. 将 key k 与 A 相乘,取其小数部分

  3. 将小数部分与另一个常数 m 相乘,向下取整

以上步骤可以总结为:

假设计算机的 word size 为 w,那么上述思想的一种实现就是:

即将 key k 与一个 w 位的常数 a 相乘,得到一个 2w 位的数,再取后者的后 w 位的前 r 位...如下图所示:

Knuth 建议:

Universal Hashing

Universal Hashing 是一种随机算法,核心思想是从一组预定义好的 hash functions 中随机选取一个来执行 hash,从概率上可以证明它的预期效果与 Simple Uniform Hashing 一致。

将在 6.046 的笔记中详细介绍。

Hash Functions For Open Addressing

在 open addressing 中,hash functions 指定 item 尝试插入的位置先后顺序

各操作实现逻辑

Insert

for i in range(m):
    if T[h(k, i)] is None:
        T[h(k, i)] = (k, v)
        return
raise 'full'

Search

for i in range(m):
    if T[h(k, i)] is None:
        return None
    elif T[h(k, i)][0] == k:
        return T[h(k, i)]
return None

Delete

注意在删除元素的时候,我们不能直接找到该元素然后将其删除,这样之前依赖于这个位置的元素都将无法找到,可以使用特殊的标记,如 "DeleteMe",Insert 将它当作是 None,但是 Search 将它当作存在的元素。

理想方案:Uniform Hashing Assumption (与 Simple Hashing Assumption 不同)

对于 Open Addressing 来说,什么样的 hash function 才是个好 hash function 呢?

  • 每个 key 的插入顺序是全排列中的任意一个的概率相同

实践方案

Linear Probing

Linear Probing 就是在发生冲突的时候,找到最近的下一个空闲位置,将 item 插入。

一个形象的例子就是停车位,通常驾驶员会从马路边的某个车位开始向一个方向寻找空闲车位,知道找到为止。

Double Hashing

顾名思义,就是使用两个 hash function 组合而成,其公式如下:

可以通过证明推导出,在一定条件下,Double Hashing 能达到 Uniform Hashing Assumption 的效果。

Table Resizing

Hash Table 应该设为多大?

  • 通常我们也不知道表应该设置为多大,所以简单地想法就是:从一个小的常数开始,按需放缩。

  • 在扩容和缩容操作之后,通常我们还需要重新计算每个元素的 hash 值,这个过程称为 Rehashing

放缩的速度

什么时候放缩

参考

2011 Fall, MIT 6.006, lecture 8-10

Last updated