30 秒速览
- 什么时候用:需要快速查找、去重、计数、分组
- 核心思想:用空间换时间,通过 key 直接计算存储位置
- 时间复杂度:平均 O(1) 查找/插入/删除
- 空间复杂度:O(n)
一、直觉建立
图书馆找书
想象你在一个拥有 100 万本书的图书馆找一本书:
方法一:线性查找
问:《算法导论》在哪?
答:从第 1 排开始一本本找吧...
时间:平均 50 万次 → O(n)
方法二:哈希表
1. 听到书名:《算法导论》
2. 脑海中计算:hash("算法导论") = 42
3. 直接去第 42 号书架取书
时间:O(1)
关键洞察:哈希表把"任意类型的 key"转换成"数组下标",实现 O(1) 访问。
生活中的哈希表
字典查词:按首字母 A-Z 分章节,直接翻到对应章节 学生档案:学号 % 100 = 柜子编号 身份证查信息:用身份证号作为 key,直接定位
Python 中的哈希表家族
# 基础款
dict # 通用键值对映射
set # 只存 key,用于判断存在性和去重
# 增强款(collections 模块)
Counter # 专门用于计数统计
defaultdict # 带默认值,避免 KeyError
# 选择指南
存储键值对 → dict
只判断存在性 → set
需要计数 → Counter
需要默认值 → defaultdict
二、核心原理
哈希函数
哈希表 = 哈希函数 + 数组
位置 index = hash(key) % 数组大小
哈希冲突
不同 key 可能计算出相同 index。
解决方案:
- 链地址法:同一位置的元素用链表串起来
- 开放地址法:占用了就找下一个空位
Python 的 dict 使用开放地址法的变体。
三、三大场景
场景 1:快速查找
触发信号:需要判断某元素是否存在
# 用 set 实现快速查找
visited = set()
if target in visited: # O(1)
...
# 用 dict 实现 key-value 查找
cache = {}
if key in cache: # O(1)
return cache[key]
场景 2:计数统计
触发信号:需要统计每个元素出现的次数
from collections import Counter
# 方法一:Counter
count = Counter(nums)
# 方法二:手动用 dict
count = {}
for num in nums:
count[num] = count.get(num, 0) + 1
# 方法三:defaultdict
from collections import defaultdict
count = defaultdict(int)
for num in nums:
count[num] += 1
场景 3:分组
触发信号:需要按某个特征把元素分组
from collections import defaultdict
# 按字符串的字母组成分组(异位词)
groups = defaultdict(list)
for word in words:
key = ''.join(sorted(word)) # 排序后作为 key
groups[key].append(word)
# 按数字的某特征分组
groups = defaultdict(list)
for num in nums:
key = num % k
groups[key].append(num)
四、典型例题
例题 1:LC 1. 两数之和
题目:给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值的两个整数,并返回它们的数组下标。
示例:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
思路
用哈希表记录已经遍历过的数字及其索引,查找 target - nums[i] 是否存在。
代码
def twoSum(nums: list[int], target: int) -> list[int]:
seen = {} # 值 → 索引
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
复杂度:时间 O(n),空间 O(n)
例题 2:LC 49. 字母异位词分组
题目:给你一个字符串数组,请你将字母异位词组合在一起。字母异位词是由相同字母重新排列形成的字符串。
示例:
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
思路
字母异位词排序后相同,用排序后的字符串作为 key 分组。
代码
from collections import defaultdict
def groupAnagrams(strs: list[str]) -> list[list[str]]:
groups = defaultdict(list)
for s in strs:
key = ''.join(sorted(s)) # 排序后作为 key
groups[key].append(s)
return list(groups.values())
复杂度:时间 O(n * k log k),k 是字符串最大长度
例题 3:LC 128. 最长连续序列
题目:给定一个未排序的整数数组 nums,找出数字连续的最长序列的长度。要求算法的时间复杂度为 O(n)。
示例:
输入:nums = [100,4,200,1,3,2]
输出:4 (最长连续序列是 [1, 2, 3, 4])
思路
关键观察:对于每个数,只有当它是一个序列的起点(num-1 不存在)时才开始计算长度。
代码
def longestConsecutive(nums: list[int]) -> int:
num_set = set(nums)
max_len = 0
for num in num_set:
# 只有当 num 是序列起点时才计算
if num - 1 not in num_set:
curr_num = num
curr_len = 1
while curr_num + 1 in num_set:
curr_num += 1
curr_len += 1
max_len = max(max_len, curr_len)
return max_len
复杂度:时间 O(n),每个数最多被访问两次
例题 4:LC 349. 两个数组的交集
题目:给定两个数组,计算它们的交集。
代码
def intersection(nums1: list[int], nums2: list[int]) -> list[int]:
set1 = set(nums1)
set2 = set(nums2)
return list(set1 & set2) # 交集
复杂度:时间 O(m + n)
例题 5:LC 454. 四数相加 II
题目:给定四个整数数组 nums1, nums2, nums3, nums4,计算有多少个元组 (i, j, k, l) 使得 nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0。
思路
把四数之和分成两组:先计算 nums1 + nums2 的所有可能和,再查找 -(nums3 + nums4)。
代码
from collections import Counter
def fourSumCount(nums1: list[int], nums2: list[int],
nums3: list[int], nums4: list[int]) -> int:
# 统计 nums1 + nums2 的所有可能和
sum12 = Counter()
for a in nums1:
for b in nums2:
sum12[a + b] += 1
# 查找 -(nums3 + nums4) 在 sum12 中出现的次数
result = 0
for c in nums3:
for d in nums4:
result += sum12[-(c + d)]
return result
复杂度:时间 O(n²),空间 O(n²)
例题 6:LC 383. 赎金信
题目:给你两个字符串 ransomNote 和 magazine,判断 ransomNote 能不能由 magazine 里面的字符构成。
代码
from collections import Counter
def canConstruct(ransomNote: str, magazine: str) -> bool:
count = Counter(magazine)
for c in ransomNote:
if count[c] == 0:
return False
count[c] -= 1
return True
复杂度:时间 O(m + n)
例题 7:LC 560. 和为 K 的子数组(前缀和 + 哈希表)
详见前缀和专题。核心是用哈希表记录前缀和出现的次数,快速找到满足条件的前缀和。
五、哈希表 vs 其他数据结构
| 操作 | 数组 | 哈希表 | 有序结构(Tree) |
|---|---|---|---|
| 查找 | O(n) | O(1) | O(log n) |
| 插入 | O(n) | O(1) | O(log n) |
| 删除 | O(n) | O(1) | O(log n) |
| 有序遍历 | O(n log n) | O(n log n) | O(n) |
| 范围查询 | O(n) | O(n) | O(log n + k) |
选择:
- 只需要快速查找 → 哈希表
- 需要有序遍历/范围查询 → 平衡树(如 SortedDict)
- 数据量小 → 数组够用
六、常见错误与陷阱
错误 1:使用可变对象作为 key
# 错误:list 不能作为 dict 的 key
d = {}
d[[1, 2]] = 3 # TypeError: unhashable type: 'list'
# 正确:用 tuple
d = {}
d[(1, 2)] = 3 # OK
错误 2:修改正在遍历的 dict/set
# 错误:遍历时修改
d = {1: 'a', 2: 'b', 3: 'c'}
for k in d:
if k == 2:
del d[k] # RuntimeError
# 正确:先收集要删除的 key
to_delete = [k for k in d if k == 2]
for k in to_delete:
del d[k]
错误 3:Counter 的陷阱
c = Counter(['a', 'b'])
print(c['c']) # 输出 0,而不是 KeyError!
# 如果需要判断是否存在,用 in
if 'c' in c: # False
...
七、复盘清单
触发信号
- 需要快速判断元素是否存在
- 需要去重
- 需要统计频率
- 需要按特征分组
- 需要记录已访问的元素
边界条件
- 空输入
- 重复元素
- 负数作为 key
- 字符串作为 key
常见错误
- 使用不可哈希的对象作为 key
- 遍历时修改 dict/set
- Counter 访问不存在的 key 返回 0
- defaultdict 忘记指定默认工厂函数
八、题目清单
基础题
| 题号 | 题目 | 难度 | 考点 |
|---|---|---|---|
| 1 | 两数之和 | Easy | 快速查找 |
| 217 | 存在重复元素 | Easy | set 去重 |
| 349 | 两个数组的交集 | Easy | set 交集 |
| 383 | 赎金信 | Easy | Counter |
| 242 | 有效的字母异位词 | Easy | Counter 比较 |
进阶题
| 题号 | 题目 | 难度 | 考点 |
|---|---|---|---|
| 49 | 字母异位词分组 | Medium | 分组 |
| 128 | 最长连续序列 | Medium | set + 起点 |
| 454 | 四数相加 II | Medium | 分组统计 |
| 560 | 和为 K 的子数组 | Medium | 前缀和+哈希 |
| 3 | 无重复字符的最长子串 | Medium | 滑动窗口+set |