哈希表 Hash Table
哈希表本质不是数据结构,是一种算法套路:用 O(1) 查询换 O(N) 遍历。看到”找两个元素满足某关系”就该条件反射。
直觉 — O(1) 查询换 O(N) 遍历
暴力解法的题大多长这样:枚举所有 (i, j) 对,看是否满足某关系。O(N²)。
哈希表做的事情非常单纯:边遍历边把”已见过的”丢进表,每来一个新元素,问表里有没有能跟我配对的。一次遍历 + 每步 O(1) 查询 = O(N)。
套路化成一句话:
把 O(N²) 的”配对枚举”,改成 O(N) 的”边走边查”。
什么时候用得上:
- 题目要求两个元素满足
f(a, b) = target(两数之和、四数相加、HashMap 找差/和/积) - 题目问”前面是否出现过 X”(最长无重复子串、包含重复元素)
- 题目要数”某种值出现了多少次”(计数、异位词分组)
- 题目想把 O(N) 的范围查询压成 O(1)(前缀和 + hash,下面讲)
set vs dict 怎么选:只关心”见过没”用 set;要存附加信息(下标、计数、上次出现的位置)用 dict。两数之和要返回下标,所以是 dict;判重就是 set。别一上来就 dict 装样子。
动画:两数之和的”先查后存”
数组 [2, 7, 11, 15],target = 9。重点看两件事:
- 每来一个新元素 x,先查表里有没有
target - x,没有再把 x 自己存进去。 - seen 字典从空慢慢长大,第 2 个元素
7来的时候,表里已经有2,立刻命中。
看完动画再回头读下面的模板,套路就刻进肌肉里了。
模板 — 两数之和、子数组和等于 k
套路一:两数之和(先查后存)
1 | |
为什么必须先查后存?nums = [3, 3], target = 6:如果先存后查,第一个 3 存进去后立刻自己查到自己,返回 [0, 0]——同一个元素配自己。先查后存把”自己”挡在表外。
套路二:是否出现过 / 判重
1 | |
注意这里换成了 set,因为不需要附加信息。
套路三:分组 / 计数(选好 key 是关键)
1 | |
选 key 的标准:可哈希 + 能唯一代表”同类”。
前缀和 + hash 组合
这是 hash 在算法题里最容易拉开差距的一招。子数组和 = prefix[j] - prefix[i],所以”子数组和等于 k”就等价于”找两个前缀和差等于 k”——又一次回到两数之和的形状。
1 | |
两个关键点:
{0: 1}不能漏:代表”什么都不取”的空前缀。少了它,从下标 0 开始的合法子数组会被漏数。- 先查后存的顺序仍然成立:先用旧的 prefix 算 count,再把当前 curr_sum 写进表。否则会把当前位置自己算成”前一段”。
这个组合一旦学会,下面这些题就都是同一道:
| 变体 | key 存什么 |
|---|---|
| 和等于 k | prefix_sum |
| 和能被 k 整除 | prefix_sum % k |
| 二叉树路径和(437) | 沿路径维护 prefix |
| 0/1 子数组数量相等 | 把 0 当 -1,再求和等于 0 |
经典题
热身(一遍写出来):
- Two Sum — 模板题
- Contains Duplicate — set 判重
- Group Anagrams — 选 key
- Longest Consecutive — set + 跳过非起点,O(N)
- Subarray Sum Equals K — 前缀和模板
进阶:
- 4Sum II — 拆成
a+b和-(c+d)两边,hash 把 O(N⁴) 砍成 O(N²)
- 4Sum II — 拆成
- Subarray Sums Divisible by K — 前缀和模 K
- Path Sum III — 前缀和 + 回溯,下树前加、回溯前删
- LRU Cache — dict + 双链表
- Design Twitter — hash 套 heap
Python 速查
1 | |
Counter 减法是滑动窗口比较字符频率、判断”能否拼成 X”的利器。
进阶:滚动哈希 Rabin-Karp
字符串匹配场景,把窗口看成 base 进制数,移动窗口时 O(1) 更新哈希:
1 | |
哈希相等还得用字符串再比一次,工程上是基本操作。
记忆法 / 易错点
- 看到”配对”想 hash:题面出现”两个数满足…”、”是否存在…”、”前面有没有…”——条件反射伸手摸 hash。
- 先查后存:永远别让当前元素配它自己。
- set vs dict:只问见没见 → set;要存下标/计数/位置 → dict。
- 前缀和 hash 一定要
{0: 1}初始化:覆盖”从头开始”的子数组。 - 遍历中改 dict 要小心:键变更可能影响后续逻辑,必要时拷贝或延后写。
- 大数据担心碰撞:自定义对象做 key 要实现
__hash__和__eq__,且两者必须一致。 - 不可变才能当 key:list 不行、tuple 可以;set 不行、frozenset 可以。
思考题
为什么两数之和必须"先查后存"?给一个"先存后查"会出错的例子。
nums = [3, 3], target = 6。先存后查:第一个 3 先写入 seen[3]=0,立刻查 6-3=3 在表里,返回 [0, 0]——同一个元素自己配自己,错误。
先查后存:第一个 3 来时表空,查不到,再写入;第二个 3 来时查到表里的 3,返回 [0, 1],正确。
前缀和 + hash 中,{0: 1} 初始化代表什么?不加会漏什么?
代表”什么都不取的空前缀,和为 0,出现过 1 次”。
当某个 curr_sum 恰好等于 k 时(即从下标 0 一路加到当前位置就是 k),代码会去查 prefix[curr_sum - k] = prefix[0]。没初始化就漏掉这种”从头开始”的子数组。
例:nums = [1, 2, 3], k = 3,子数组 [1, 2] 就是靠 prefix[0]=1 数到的。
Counter 减法 c1 - c2 为什么只保留正数?什么时候有用?
Counter 是”多重集合”,元素数不能负。减法即”移除”,移除超过持有量就归零。
用法:
- 判断
word能否由available的字母拼成:Counter(word) - Counter(available)为空则可以。 - 找缺什么字母:
Counter(required) - Counter(have)。 - 滑动窗口比较字符频率。