#️⃣ 哈希表 — 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 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 349)

题目描述:给定两个数组 nums1nums2,返回它们的交集。输出结果中的每个元素必须是唯一的。

示例: 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)

变体练习

成就解锁:哈希术士 — 掌握查找配对、频率统计、分组归类三大哈希应用模式
LeetCode AC验证:LC 1 Two Sum ✅ | LC 49 Group Anagrams ✅ | LC 349 Intersection of Two Arrays ✅

📝 课后练习

  1. LC 242 有效的字母异位词
  2. LC 202 快乐数(Floyd 或集合检测循环)
  3. LC 205 同构字符串(双向映射)
  4. LC 219 存在重复元素 II(滑动窗口+集合)
  5. LC 454 四数相加 II(分组哈希)

🔑 本课要点回顾

📚 扩展阅读

思考题:如果两个数组的交集需要保留重复元素(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 缓存?提示:需要额外的频率哈希表。