Hash Tables
- 目标:维护一组可能发生演变的事物集合
- 插入:增加一个新的记录
- 删除:删除一个既有的指定记录
- 查找:检查一个指定的记录
- 关键在于对于键的处理,但不维护元素的相对顺序
- 所有的操作的时间复杂度为 O ( 1 ) O(1) O(1)
- 注意:
- 哈希表很容易实现地很糟糕,从而无法保证常数时间操作
- 不存在“最糟糕”情形——病态数据也能保证适当的时间复杂度
- 应用:去重
- 输入:一个输入对象流
- 目标:去除输入的重复值
- 解决方案:输入——检查——存在即忽略,缺席即保留
- 应用:2-SUM问题
- 输入:一组 n n n个整数,目标和 t t t
- 目标:是否存在两个数, x + y = t x + y = t x+y=t?
- 解决方案1:对数组排序,对于每一个
x
x
x,使用二分查找寻找是否存在
t
−
x
t-x
t−x
- 时间复杂度 O ( n log n ) O(n \log n) O(nlogn)
- 解决方案2:将数据插入哈希表,对每一个
x
x
x,检查哈希表中是否存在
t
−
x
t - x
t−x
- 时间复杂度 O ( n ) O(n) O(n)
- 对于包含所有目标数据的集合
U
U
U,希望能够维护一个动态的集合
S
⊆
U
S \subseteq U
S⊆U,后者的尺寸更加合理,更易实现
- 基于 U U U的数组实现: O ( 1 ) O(1) O(1)时间,但是已经 O ( ∣ U ∣ ) O(|U|) O(∣U∣)空间
- 基于 S S S的链表实现: O ( ∣ S ∣ ) O(|S|) O(∣S∣)空间,但是已经 O ( ∣ S ∣ ) O(|S|) O(∣S∣)时间
- 哈希表期望: O ( 1 ) O(1) O(1)的时间, O ( ∣ S ∣ ) O(|S|) O(∣S∣)的空间
- 实现
- 设置 n ≈ ∣ S ∣ n \approx |S| n≈∣S∣个桶(问题简化,不考虑动态分配空间的情形)
- 选择一个合适的哈希函数,将 U U U映射到 [ 0 , n − 1 ] [0, n - 1] [0,n−1]
- 将标签输入哈希函数,得到的键值用于确定存储位置
- 问题:碰撞???
- 生日悖论:当整个哈希表还没有装入很多元素时,就会开始出现碰撞
- 碰撞:对于一个哈希函数
h
h
h,对于两个不同元素
x
,
y
∈
U
x, y \in U
x,y∈U,有
h
(
x
)
=
h
(
y
)
h(x) = h(y)
h(x)=h(y)
- 分开成链
- 在碰撞时,每个桶维护一个链表,查找是沿着链表匹配
- 插入时,时间复杂度 O ( 1 ) O(1) O(1);查找或删除则需要 O ( list length ) O(\text{list length}) O(list length)时间
- 列表的长度受哈希函数影响,对 n n n个桶插入 m m m个数据,在最理想情况下长度为 m / n m / n m/n(均匀分布),最糟糕情况下 m m m(全都堆一块)
- 这种实现下,性能完全由哈希函数决定
- 开放地址
- 向后探查,直到找到一个空位置(线性探查)
- 准备两个哈希函数,第一个哈希函数对应的位置不为空时,后一个作为步长不断向后探查(双重哈希)
- 同样,性能也完全由哈希函数决定
- 分开成链增加对空间的需求,开放地址需要对删除情形额外处理
- 分开成链
- 一个好的哈希函数
- 在哈希表中均匀摊开数据——随机方法?插入就没法查了……
- 要么易于存储键值,要么能够常数时间内得到键值
- 快速比较麻烦的哈希函数
- 哈希函数(由输入串产生一个大数字) + 压缩函数(数字的压缩表示)
- 桶数目的选择(压缩效果)
- 一个质数
- 尽量不要接近2的幂
- 也不要接近10的幂
Universal Hashing
-
Load:一个哈希表的平均装载量(装载总量 / 桶总数)
- 只有当load为常数时,操作的常数时间才能得到保证
- 对于开放地址,我们希望load尽可能小于1(始终不是很满)
- 需要控制好load
-
要点
- 好的性能,需要控制load
- 好的性能,需要好的哈希函数
-
任何一个哈希函数,都存在一个病态数据集,使其无法保证均匀地摊开数据
- 加密哈希函数(SHA-2),发现病态数据集变得不可行
-
使用随机化方法,一组哈希函数,每次随机选一个,随机化能够尽可能均匀地散布数据
-
通用哈希函数
- H \mathcal{H} H为一组哈希函数,从 U U U映射到 n n n个桶
- H \mathcal{H} H是通用的,当且仅当 P r h ∈ H [ h ( x ) = h ( y ) , x ≠ y ] ≤ 1 n Pr_{h \in \mathcal{H}}[h(x) = h(y),\ x \neq y] \le \frac 1n Prh∈H[h(x)=h(y), x=y]≤n1
- 出现碰撞的概率足够小(不超过均匀分布概率)
- 1 n \frac 1n n1的函数保证将 k k k映射到 i i i?如果有 n n n个全映射哈希函数,能够保证这组哈希函数是通用的
-
通用哈希函数能够保证成链实现的哈希表(随机选择、桶数相比于数据集大小可观)的操作时间在常数(因为随机化使得每一个同对应链表的长度期望为常数)
-
对于开放地址实现,假设所有 n ! n! n!中探查等概率(实际基本不可能),期望插入时间为 1 1 − α \frac 1{1 - \alpha} 1−α1
- 在线性探查实现中,理想假设已经失效,假设探查是独立的均匀分布,插入时间基本维持 1 ( 1 − α ) 2 \frac 1{(1 - \alpha)^2} (1−α)21
- 性能受到探查策略的极大影响
Bloom Filter
- 一个哈希表的变种,允许错误发生
- 特点:快速的插入和查找
- 优势
- 更小的空间需求(无论何种实现)
- 劣势
- 并不存储对应的对象
- 不能进行删除
- 较小的假阳性概率
- 应用
- 早期的拼写检查器
- 禁止密码列表
- 网络路由器(小内存,超快读写)
- 组成:数组 + 若干哈希函数
- 数组的每一项都是一个二值位( n n n个bit)
- 插入对象集合为 S S S,那么 n / ∣ S ∣ n / |S| n/∣S∣即为每个对象占用的位数,可以显著降低存储占用
- k k k个哈希函数,决定某个对象对应的位将被激活
- 插入:对输入 x x x,将 A [ h i ( x ) ] A[h_i(x)] A[hi(x)]置为1(无论是否已经置为1)
- 查找:对目标 x x x,如果所有 A [ h i ( x ) ] A[h_i(x)] A[hi(x)]均为1,则哈希表中存在该目标——存在假阳性的可能
- 能否在较小的单位对象位占用的情况下保证较小的假阳性率?——权衡
- 空间越大,出错率越低
- k k k的选择?在给定 b = n / ∣ S ∣ b = n / |S| b=n/∣S∣的情况下,出现假阳性的误差率 ϵ \epsilon ϵ可以被 k k k最小化
- k ≈ ( ln 2 ) ⋅ b k \approx (\ln 2) \cdot b k≈(ln2)⋅b
- ϵ ≈ ( 1 2 ) ( l n 2 ) ⋅ b \epsilon \approx (\frac12)^{(ln 2) \cdot b} ϵ≈(21)(ln2)⋅b