数据结构 / 04
哈希表
什么是哈希表
哈希表(Hash Table)是一种通过哈希函数将键(key)映射到存储位置的数据结构,从而实现近乎 O(1) 的查找、插入和删除操作。它是算法面试中出现频率最高的数据结构之一。
核心原理
- 哈希函数:将任意键转换为数组下标。理想的哈希函数应当分布均匀、计算高效。
- 哈希冲突:不同的键可能映射到同一下标,必须有冲突处理策略。
常见的冲突解决方式:
| 方法 | 思路 | 优缺点 |
|---|---|---|
| 链地址法(Chaining) | 每个槽位维护一个链表,冲突元素追加到链表 | 实现简单;极端情况退化为 O(n) |
| 开放寻址法(Open Addressing) | 冲突时按探测序列寻找下一个空槽 | 缓存友好;装载因子高时性能下降 |
Python 的
dict内部采用开放寻址法。
时间复杂度
| 操作 | 平均 | 最坏 |
|---|---|---|
| 查找 | O(1) | O(n) |
| 插入 | O(1) | O(n) |
| 删除 | O(1) | O(n) |
最坏情况出现在所有键发生冲突时,但在合理的哈希函数下几乎不会发生。
Python 中的哈希表
Python 提供了多种基于哈希的内置类型,面试中最常用的有四个:dict、Counter、defaultdict 和 set。
dict
dict 是 Python 最基础的哈希表实现。
d = {"apple": 3, "banana": 5}
d["cherry"] = 7 # 增
d["apple"] = 10 # 改(覆盖旧值)
val = d.get("grape", 0) # 查,不存在返回默认值
del d["banana"] # 删
val = d.pop("cherry", None)
if "apple" in d: # 判断键是否存在
print(d["apple"])
遍历与推导式:
for k, v in d.items(): # 遍历键值对;也可用 .keys()、.values()
print(k, v)
squared = {x: x ** 2 for x in [1, 2, 3, 4]}
# {1: 1, 2: 4, 3: 9, 4: 16}
collections.Counter
Counter 专门用于计数,是 dict 的子类。
from collections import Counter
cnt = Counter(["apple", "banana", "apple", "cherry", "banana", "apple"])
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})
cnt["grape"] # 不存在返回 0,不抛异常
cnt.most_common(2) # [('apple', 3), ('banana', 2)]
freq = Counter("abracadabra") # 字符频率统计
# Counter({'a': 5, 'b': 2, 'r': 2, 'c': 1, 'd': 1})
collections.defaultdict
defaultdict 在访问不存在的键时自动创建默认值,避免 KeyError。
from collections import defaultdict
# defaultdict(int):默认值 0,适合计数
counter = defaultdict(int)
for ch in "hello":
counter[ch] += 1 # {'h': 1, 'e': 1, 'l': 2, 'o': 1}
# defaultdict(list):默认值空列表,适合分组
groups = defaultdict(list)
for category, item in [("fruit", "apple"), ("fruit", "banana"), ("veggie", "carrot")]:
groups[category].append(item)
# defaultdict(set):去重分组,常用于建图
graph = defaultdict(set)
for u, v in [(1, 2), (1, 3), (2, 3)]:
graph[u].add(v)
graph[v].add(u)
set
set 是基于哈希表的无序集合,元素唯一,支持 O(1) 成员检查。
s = set([1, 2, 2, 3]) # 自动去重 -> {1, 2, 3}
s.add(4)
s.discard(2) # 不存在也不报错
if 3 in s: # O(1) 成员检查
print("found")
a, b = {1, 2, 3, 4}, {3, 4, 5, 6}
a & b # 交集 {3, 4}
a | b # 并集 {1, 2, 3, 4, 5, 6}
a - b # 差集 {1, 2}
a ^ b # 对称差集 {1, 2, 5, 6}
面试提示:当题目要求”判断是否存在”或”去重”时,优先考虑
set。
高频题型与解题模式
两数之和模式
核心思路:遍历数组时,用哈希表记录已见过的元素及其下标。对于当前元素 num,检查 target - num 是否在哈希表中。
def two_sum(nums: list[int], target: int) -> list[int]:
seen = {} # val -> index
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)。这个”边遍历边查表”的模式非常通用,可以推广到 k 数之和、配对问题等。
计数问题
字母异位词分组(LC 49):将每个单词排序后作为键,值为该键对应的所有原始单词。
def group_anagrams(strs: list[str]) -> list[list[str]]:
groups = defaultdict(list)
for s in strs:
key = tuple(sorted(s))
groups[key].append(s)
return list(groups.values())
前缀和 + 哈希表(LC 560 和为 K 的子数组):维护前缀和的计数哈希表。子数组 nums[i..j] 的和等于 prefix[j+1] - prefix[i],因此对于每个前缀和,查找 prefix_sum - k 出现的次数。
def subarray_sum(nums: list[int], k: int) -> int:
count = 0
prefix_sum = 0
prefix_count = defaultdict(int)
prefix_count[0] = 1 # 空前缀
for num in nums:
prefix_sum += num
count += prefix_count[prefix_sum - k]
prefix_count[prefix_sum] += 1
return count
滑动窗口 + 哈希表
找所有字母异位词(LC 438):维护一个固定大小的窗口,用 Counter 统计窗口内字符频率,与目标频率比较。
def find_anagrams(s: str, p: str) -> list[int]:
if len(s) < len(p):
return []
p_count = Counter(p)
s_count = Counter(s[:len(p)])
result = []
if s_count == p_count:
result.append(0)
for i in range(len(p), len(s)):
# 右端加入
s_count[s[i]] += 1
# 左端移出
left = s[i - len(p)]
s_count[left] -= 1
if s_count[left] == 0:
del s_count[left]
if s_count == p_count:
result.append(i - len(p) + 1)
return result
经典题目
按难度分组的高频哈希表题目:
Easy
| # | 题目 | 关键思路 |
|---|---|---|
| 1 | 两数之和 | 哈希表存已遍历元素 |
| 217 | 存在重复元素 | set 判重 |
| 242 | 有效的字母异位词 | Counter 比较频率 |
Medium
| # | 题目 | 关键思路 |
|---|---|---|
| 49 | 字母异位词分组 | 排序后作键,defaultdict 分组 |
| 128 | 最长连续序列 | set + 只从序列起点开始计数 |
| 347 | 前 K 个高频元素 | Counter.most_common 或桶排序 |
| 438 | 找到字符串中所有字母异位词 | 滑动窗口 + Counter |
| 560 | 和为 K 的子数组 | 前缀和 + 哈希表计数 |
小结
| 要点 | 说明 |
|---|---|
| O(1) 查找 | 哈希表的核心优势,将暴力 O(n) 查找降为 O(1) |
| 空间换时间 | 几乎所有哈希表解法都是用额外空间换取时间复杂度的降低 |
| 选对工具 | 计数用 Counter,分组用 defaultdict(list),判重用 set,通用映射用 dict |
| 常见搭配 | 哈希表 + 前缀和、哈希表 + 滑动窗口、哈希表 + 排序 |
掌握哈希表的关键不在于记住 API,而在于识别”需要快速查找/计数/去重”的场景,并选择合适的数据结构来实现。