哈希表
- 什么是哈希表
- 哈希函数
- 冲突与解决方案
- 查询数据
- 性能与填装因子
- 应用案例
什么是哈希表
哈希表(也叫散列表)是一种通过哈希函数将键映射到数组位置,从而实现快速查找的数据结构。它可能是最有用的复杂数据结构——Python 的字典(dict)、Java 的 HashMap、JavaScript 的对象都是哈希表。
哈希表存储的是键值对(key-value),比如:
键(key) 值(value)
───────── ─────────
"apple" → 0.67
"milk" → 1.49
"avocado"→ 1.49
哈希函数
哈希函数是哈希表的核心。它接收一个键,返回一个数字(数组索引)。好的哈希函数满足两个要求:
- 一致的:同样的输入总是返回同样的数字
- 映射均匀:不同的输入尽量映射到不同的位置
存储过程(以 5 个箱子的数组为例):
第1步: 计算键的哈希值
Hash("Joe") = 4928
第2步: 用哈希值对数组长度取余(mod 运算),得到存储位置
4928 mod 5 = 3
第3步: 存入 3 号箱子
箱子: 0 1 2 3 4
[ ] [ ] [ ] [Joe M] [ ]
↑
存入这里
重复这个过程存储其他数据:
箱子: 0 1 2 3 4
[ ] [Sue F] [ ] [Joe M] [Dan M]
冲突与解决方案
几乎不可能编写出完全不冲突的哈希函数。当两个键映射到同一个位置时,就发生了冲突。
Nell 的哈希值为 6276,6276 mod 5 = 1
但 1 号箱子已经有 Sue 了!
箱子: 0 1 2 3 4
[ ] [Sue F] [ ] [Joe M] [Dan M]
↑
Nell 也要放这里 → 冲突!
链地址法:在冲突的位置存储一个链表。
箱子: 0 1 2 3 4
[ ] [Sue → Nell] [ ] [Joe → Ally → Bob] [Dan]
↑ 链表 ↑ 链表
开放地址法:冲突时计算下一个候补位置,直到找到空位。
查询数据
查询 Dan 的性别:
1. Hash("Dan") = 1539, 1539 mod 5 = 4
2. 查看 4 号箱子 → 键是 Dan → 找到了!值是 M
查询 Ally 的性别:
1. Hash("Ally") = 9143, 9143 mod 5 = 3
2. 查看 3 号箱子 → 键是 Joe,不是 Ally
3. 顺着链表找 → Joe → Ally → 找到了!值是 F
性能与填装因子
| 操作 | 平均情况 | 最糟情况 |
|---|---|---|
| 查找 | O(1) | O(n) |
| 插入 | O(1) | O(n) |
| 删除 | O(1) | O(n) |
平均情况下,哈希表的各种操作都是 O(1)——常量时间,不管表有多大都一样快。最糟情况(所有键都映射到同一位置)退化为 O(n)。
填装因子 = 元素数 / 位置总数。
填装因子 = 0.4(5个位置中用了2个)→ 比较空,冲突少
[ ] [■] [ ] [■] [ ]
经验规则:一旦填装因子超过 0.7,就该调整散列表的长度(通常增长一倍),然后重新哈希所有元素。
应用案例
模拟映射关系:电话簿(姓名→电话)、DNS 解析(网址→IP)
phone_book = {}
phone_book["jenny"] = 8675309
phone_book["emergency"] = 911
print(phone_book["jenny"]) # 8675309
防止重复:投票站检查是否已投票
voted = {}
def check_voter(name):
if voted.get(name):
print("kick them out!")
else:
voted[name] = True
print("let them vote!")
缓存:网站将计算结果记住,避免重复计算
cache = {}
def get_page(url):
if cache.get(url):
return cache[url] # 直接返回缓存
data = get_data_from_server(url)
cache[url] = data # 存入缓存
return data
小结
- 哈希表 = 哈希函数 + 数组,用键快速定位值
- 冲突不可避免,用链地址法或开放地址法解决
- 平均情况下查找、插入、删除都是 O(1)
- 填装因子超过 0.7 就需要扩容
- 适合模拟映射、防止重复、缓存数据