#️⃣ 哈希表 — O(1)查找的魔法
哈希表是算法的加速器,用空间换时间,O(n)变O(1)
📖 哈希表的核心概念
哈希表(Hash Table)是通过哈希函数将键映射到数组下标,实现 O(1) 平均时间的查找、插入和删除。它是算法面试中最重要的数据结构之一——几乎任何"查找配对"、"统计频率"、"去重"的问题都可以用哈希表优化。理解哈希冲突的解决方式(链地址法、开放寻址法)是深入理解的关键。
哈希表的工作原理:
key → hash(key) → index → bucket[index]
示例 (取模哈希, size=7):
"abc" → hash("abc")=38 → 38%7=3 → bucket[3]
"xyz" → hash("xyz")=55 → 55%7=6 → bucket[6]
"foo" → hash("foo")=10 → 10%7=3 → bucket[3] ← 冲突!
冲突解决 - 链地址法:
bucket[0] → None
bucket[1] → None
bucket[2] → None
bucket[3] → ["abc"] → ["foo"] ← 同一个桶
bucket[4] → None
bucket[5] → None
bucket[6] → ["xyz"]
时间复杂度:
┌──────────┬──────────┬──────────┐
│ 操作 │ 平均 │ 最坏 │
├──────────┼──────────┼──────────┤
│ 查找 │ O(1) │ O(n) │
│ 插入 │ O(1) │ O(n) │
│ 删除 │ O(1) │ O(n) │
└──────────┴──────────┴──────────┘
最坏: 所有key冲突到同一个桶
1. 哈希表的面试应用模式
模式一:查找配对
两数之和:对每个元素,查找 complement 是否在哈希表中。
O(n²) → O(n)
模式二:统计频率
字符/单词频率统计,然后排序/找TopK。
O(n) 统计 + O(k log k) 排序
模式三:分组归类
字母异位词分组:将排序后的字符串作为 key。
同构字符串:映射关系作为 key。
2. Python 哈希操作速查
# dict(哈希字典)
d = {'a': 1, 'b': 2}
d['c'] = 3 # 插入/更新
x = d.get('d', 0) # 查找,不存在返回0
del d['a'] # 删除
'a' in d # 判断key存在
# set(哈希集合)
s = {1, 2, 3}
s.add(4) # 添加
s.discard(5) # 删除(不存在不报错)
4 in s # 判断存在
s1 & s2 # 交集
s1 | s2 # 并集
# Counter(频率统计)
from collections import Counter
c = Counter('aabccc')
# Counter({'c': 3, 'a': 2, 'b': 1})
c.most_common(2) # Top2: [('c',3), ('a',2)]
🎯 题目一:两数之和 (LC 1)
题目描述:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值的两个整数,并返回它们的数组下标。
示例: nums = [2, 7, 11, 15], target = 9
遍历过程:
i=0, num=2: complement=7, 不在map → map={2:0}
i=1, num=7: complement=2, 在map! → 返回[0, 1]
核心思想: 查找 complement = target - num
哈希表让查找从 O(n) 降到 O(1)
思路分析
一遍哈希法:
遍历数组,对每个元素 num:
1. 计算 complement = target - num
2. 如果 complement 在哈希表中,返回 [map[complement], i]
3. 否则将 num 和下标存入哈希表
时间复杂度:O(n),空间复杂度:O(n)
代码实现
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hash_map = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hash_map:
return [hash_map[complement], i]
hash_map[num] = i
return []
复杂度分析
时间复杂度:O(n) — 一次遍历
空间复杂度:O(n) — 哈希表
变体练习
- LC 167 两数之和 II(有序数组,双指针 O(1) 空间)
- LC 454 四数相加 II(分组哈希)
🎯 题目二:字母异位词分组 (LC 49)
题目描述:给你一个字符串数组,请你将字母异位词组合在一起。字母异位词是由相同字母重新排列形成的字符串。
示例: strs = ["eat","tea","tan","ate","nat","bat"]
排序后作为key:
"eat" → "aet" → key="aet"
"tea" → "aet" → key="aet" ← 同组!
"tan" → "ant" → key="ant"
"ate" → "aet" → key="aet" ← 同组!
"nat" → "ant" → key="ant" ← 同组!
"bat" → "abt" → key="abt"
结果:
{"aet": ["eat","tea","ate"],
"ant": ["tan","nat"],
"abt": ["bat"]}
思路分析
方法一:排序作为 key:
将每个字符串排序,排序后的字符串作为哈希表的 key。
字母异位词排序后一定相同。
时间复杂度:O(n · k log k),k 为字符串平均长度
方法二:计数作为 key:
用长度为 26 的计数数组作为 key(转为元组)。
字母异位词的字母频率一定相同。
时间复杂度:O(n · k),空间复杂度:O(n · k)
代码实现
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
from collections import defaultdict
groups = defaultdict(list)
for s in strs:
key = tuple(sorted(s))
groups[key].append(s)
return list(groups.values())
复杂度分析
时间复杂度:O(n · k log k) — n 个字符串,每个排序 O(k log k)
空间复杂度:O(n · k) — 存储所有字符串
方法二的计数法在字符串很长时更优(O(nk) vs O(nk log k)),但 Python 中 tuple(sorted(s)) 实现更简洁。面试时两种都提到。
变体练习
- LC 242 有效的字母异位词(简单版,判断两个字符串是否为异位词)
- LC 49 进阶:不用排序,用计数法
🎯 题目三:两个数组的交集 (LC 349)
题目描述:给定两个数组 nums1 和 nums2,返回它们的交集。输出结果中的每个元素必须是唯一的。
示例: nums1 = [1,2,2,1], nums2 = [2,2]
方法一: 集合交集
set1 = {1, 2}
set2 = {2}
交集 = {2} → [2]
方法二: 排序+双指针
排序: [1,1,2,2] 和 [2,2]
双指针对撞:
p1=0(1), p2=0(2) → 1<2 → p1++
p1=2(2), p2=0(2) → 2==2 → 加入结果, p1++, p2++
p1=3(2), p2=1(2) → 2==2 → 跳过(已加过), p1++, p2++
思路分析
方法一:集合操作(推荐):
将两个数组转为集合,取交集。
时间复杂度:O(n+m),空间复杂度:O(n+m)
方法二:排序+双指针:
两个数组排序,双指针找共同元素。
时间复杂度:O(n log n + m log m),空间复杂度:O(1)
代码实现
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1) & set(nums2))
复杂度分析
时间复杂度:O(n+m)
空间复杂度:O(n+m)
变体练习
- LC 350 两个数组的交集 II(保留重复,频率取最小)
- LC 202 快乐数(集合检测循环)
成就解锁:哈希术士 — 掌握查找配对、频率统计、分组归类三大哈希应用模式
LeetCode AC验证:LC 1 Two Sum ✅ | LC 49 Group Anagrams ✅ | LC 349 Intersection of Two Arrays ✅
📝 课后练习
- LC 242 有效的字母异位词
- LC 202 快乐数(Floyd 或集合检测循环)
- LC 205 同构字符串(双向映射)
- LC 219 存在重复元素 II(滑动窗口+集合)
- LC 454 四数相加 II(分组哈希)
🔑 本课要点回顾
- 哈希表:O(1) 平均查找,用空间换时间
- 查找配对:complement = target - num,一遍哈希
- 字母异位词分组:排序后的字符串作为 key
- 集合操作:交集 &、并集 |、差集 -
- Counter:频率统计神器,most_common() 找 TopK
- 哈希冲突:链地址法最常见,好的哈希函数减少冲突
思考题:如果两个数组的交集需要保留重复元素(LC 350),哈希表该如何处理?提示:用 Counter 统计频率,取 min 频率。
🔬 哈希表与LRU缓存
哈希表 + 双向链表是 LRU 缓存的经典实现,这是系统设计面试的高频题,也是哈希表应用的重要进阶。
LRU (Least Recently Used) 缓存:
容量=3, 操作序列: get(1), put(2,b), put(3,c), get(2), put(4,d)
初始: {1:a}
双向链表: a ⇄ HEAD
put(2,b): {1:a, 2:b}
链表: a ⇄ b ⇄ HEAD (b最新)
put(3,c): {1:a, 2:b, 3:c}
链表: a ⇄ b ⇄ c ⇄ HEAD (c最新)
get(2): 访问b → 移到最新
链表: a ⇄ c ⇄ b ⇄ HEAD
put(4,d): 容量满! 淘汰a(最久未用)
{2:b, 3:c, 4:d}
链表: c ⇄ b ⇄ d ⇄ HEAD
核心: 哈希表O(1)查找 + 链表O(1)调整顺序
LRU 缓存结合了哈希表 O(1) 查找和双向链表 O(1) 插入/删除的优势。哈希表存 key → 链表节点的映射,链表维护访问顺序。
设计要点:
1. 哈希表:key → Node,O(1) 查找
2. 双向链表:维护访问顺序,最新访问移到尾部
3. 虚拟头尾节点:简化链表操作
4. get:查找+移到尾部
5. put:插入/更新+容量检查+淘汰
所有操作 O(1)
思考题:LFU(最不经常使用)缓存与 LRU 有什么区别?如何实现 O(1) 的 LFU 缓存?提示:需要额外的频率哈希表。