441_数组与哈希表面试题¶
一句话说明¶
数组和哈希表是最基础的数据结构,哈希表用空间换时间,把 O(n) 查找变成 O(1),是优化暴力解法的第一反应。
核心知识点¶
白话理解¶
数组像一排座位,按位置找人 O(1) 但按名字找人 O(n)。哈希表像花名册,直接按名字查 O(1),代价是需要额外空间存花名册。面试里凡是"找重复""找配对""统计频率",第一反应就是哈希表。
常用操作时间复杂度¶
| 操作 | 数组 | 哈希表(平均) |
|---|---|---|
| 访问 | O(1) | O(1) |
| 查找 | O(n) | O(1) |
| 插入末尾 | O(1) | O(1) |
| 插入中间 | O(n) | O(1) |
| 删除 | O(n) | O(1) |
经典题目与解法¶
题目1:两数之和(LeetCode 1,经典哈希)¶
题意:给定数组 nums 和目标值 target,找出两个数的下标使其和等于 target。
def two_sum(nums, target):
"""
暴力法 O(n²),哈希法 O(n)
思路:遍历时用哈希表记录"之前见过的数",
对当前数 num,查哈希表里有没有 target-num
"""
seen = {} # 哈希表:{数值: 下标}
for i, num in enumerate(nums):
complement = target - num # 需要的另一个数
if complement in seen: # 在之前见过的数里找到了
return [seen[complement], i] # 返回两个下标
seen[num] = i # 记录当前数和它的下标
return [] # 没有找到(题目保证有解)
# 测试
print(two_sum([2, 7, 11, 15], 9)) # [0, 1](2+7=9)
print(two_sum([3, 2, 4], 6)) # [1, 2](2+4=6)
print(two_sum([3, 3], 6)) # [0, 1](3+3=6)
时间复杂度:O(n) 一次遍历
空间复杂度:O(n) 哈希表最多存 n 个元素
题目2:字母异位词分组(LeetCode 49)¶
题意:给定字符串列表,把字母异位词(字母相同、顺序不同的词)分组。
from collections import defaultdict
def group_anagrams(strs):
"""
思路:字母异位词排序后一定相同('eat' 和 'tea' 排序后都是 'aet')
用排序结果作哈希表的键,相同键的词分到一组
"""
groups = defaultdict(list) # 默认值是空列表
for word in strs:
key = ''.join(sorted(word)) # 排序作为键:'eat'→'aet'
groups[key].append(word) # 加入对应分组
return list(groups.values()) # 返回所有分组
# 测试
words = ["eat", "tea", "tan", "ate", "nat", "bat"]
result = group_anagrams(words)
print(result)
# [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]
时间复杂度:O(n × k log k),n 是词数,k 是最长词长度(排序)
空间复杂度:O(n × k)
进阶优化:用字母计数替代排序,复杂度降到 O(n × k)
def group_anagrams_v2(strs):
groups = defaultdict(list)
for word in strs:
count = [0] * 26 # 26个字母的计数
for c in word:
count[ord(c) - ord('a')] += 1 # 统计每个字母出现次数
key = tuple(count) # 用计数元组作键(可哈希)
groups[key].append(word)
return list(groups.values())
题目3:最长连续序列(LeetCode 128,哈希集合)¶
题意:给定未排序的整数数组,找出最长的连续数字序列的长度,要求 O(n)。
def longest_consecutive(nums):
"""
思路:用集合存所有数字,对每个序列的起点(num-1不在集合中)
开始向后数,统计序列长度
"""
num_set = set(nums) # 转成集合,O(1)查找
max_len = 0
for num in num_set:
if num - 1 not in num_set: # 只从序列起点开始数
current = num
length = 1
while current + 1 in num_set: # 向后延伸
current += 1
length += 1
max_len = max(max_len, length)
return max_len
# 测试
print(longest_consecutive([100, 4, 200, 1, 3, 2])) # 4(1,2,3,4)
print(longest_consecutive([0, 3, 7, 2, 5, 8, 4, 6, 0, 1])) # 9
时间复杂度:O(n) 每个数最多被访问两次(外循环 + while 循环)
空间复杂度:O(n) 集合存储
面试技巧¶
- 看到"找配对/补数"就想哈希表:complement = target - num 是万能套路
- 集合去重比排序快:去重用 set,查存在 O(1) 比排序 O(n log n) 快
- defaultdict 省去初始化判断:
defaultdict(list)比if key not in d: d[key] = []优雅 - 说清楚时间/空间权衡:哈希表用空间换时间,面试官期待你主动提
- Counter 统计频率神器:
from collections import Counter; Counter(nums)一行搞定
速查表¶
# 哈希表常用操作
d = {}
d[key] = value # 插入/更新
value = d.get(key, 0) # 安全取值,不存在返回0
key in d # 判断存在,O(1)
# defaultdict 自动初始化
from collections import defaultdict
d = defaultdict(int) # 默认值0
d = defaultdict(list) # 默认值[]
d = defaultdict(set) # 默认值set()
# Counter 计频
from collections import Counter
freq = Counter([1,1,2,3,3,3]) # {3:3, 1:2, 2:1}
freq.most_common(2) # [(3,3), (1,2)] 最常见的2个
# 集合操作
s = set(nums) # 去重
x in s # O(1)查找
s1 & s2 # 交集
s1 | s2 # 并集
s1 - s2 # 差集
| 题型 | 思路 | 工具 |
|---|---|---|
| 两数之和 | 哈希记录已见数 | dict |
| 找重复 | 哈希计数 | Counter |
| 分组/归类 | 哈希 key 分组 | defaultdict(list) |
| 去重+快速查找 | 集合 | set |
| 前缀和查询 | 哈希记前缀 | dict |