Dictionary Problem & Implementation
Dictionary Problem
dictionary, map, associated array 以及 symbol table 实际上都是指同一种抽象数据类型(ADT),它维护着一个键值对(这里记为 item)集合,支持以下方法:
方法
描述
将一个 item 加入到集合中
从集合中删除 item
如果 key 存在,则返回对应的 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,但这会产生两个问题:
keys 必须是非负整数 解决方案:"pre-hash" keys to integers (注意理解这里的 pre-hash)
如果 key 的范围很大,将造成空间浪费 解决方案:"hashing"
Hashing
chaining:hash function 指定 item 插入的位置,插入位置相同的 key 用链表串联起来
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
分为三步:
确定一个 0 到 1 之间的常数 A
将 key k 与 A 相乘,取其小数部分
将小数部分与另一个常数 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
Search
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