1. 代码训练营第七日:哈希与双指针算法实战解析
今天是我们代码训练营的第七天,重点攻克四道经典算法题目:454.四数相加II、383.赎金信、15.三数之和和18.四数之和。这几道题涵盖了哈希表和双指针两大核心算法技巧,是面试中90%以上公司都会考察的高频题型。我在刷题和实际面试中发现,能否熟练解决这类问题直接决定了候选人能否通过技术面。
2. 454. 四数相加II:哈希表的经典应用
2.1 问题重述与暴力解法分析
给定四个整数数组nums1、nums2、nums3、nums4,计算有多少个元组(i,j,k,l)满足:
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
最直观的暴力解法是四重循环枚举所有可能的组合,时间复杂度O(n^4)。当n=200时,操作次数将达到1.6亿次,显然不可行。
2.2 哈希表优化思路
我们可以将问题拆分为两组两数之和:
- 先计算nums1和nums2所有元素两两相加的和,存入哈希表记录出现次数
- 再计算nums3和nums4所有元素两两相加的和的相反数,查询哈希表
python复制def fourSumCount(nums1, nums2, nums3, nums4):
from collections import defaultdict
hashmap = defaultdict(int)
count = 0
# 计算nums1和nums2的两数之和
for n1 in nums1:
for n2 in nums2:
hashmap[n1 + n2] += 1
# 计算nums3和nums4的两数之和的相反数
for n3 in nums3:
for n4 in nums4:
target = -(n3 + n4)
if target in hashmap:
count += hashmap[target]
return count
2.3 复杂度分析与优化技巧
时间复杂度从O(n^4)优化到O(n^2),空间复杂度O(n^2)。实际测试中,当n=200时,优化后的算法只需约4万次操作。
注意:使用defaultdict可以避免键不存在的判断,比普通字典更简洁高效。在Python中,Counter也可以实现类似功能。
3. 383. 赎金信:字符频率统计实战
3.1 问题理解与边界条件
给定一个赎金信字符串和一个杂志字符串,判断杂志中的字符能否组成赎金信。关键点:
- 杂志中的字符只能使用一次
- 赎金信中的每个字符都必须在杂志中出现足够次数
- 大小写敏感
3.2 两种实现方式对比
方法一:使用数组作为哈希表
python复制def canConstruct(ransomNote, magazine):
if len(ransomNote) > len(magazine):
return False
count = [0] * 26
for c in magazine:
count[ord(c) - ord('a')] += 1
for c in ransomNote:
index = ord(c) - ord('a')
count[index] -= 1
if count[index] < 0:
return False
return True
方法二:使用Counter对象
python复制from collections import Counter
def canConstruct(ransomNote, magazine):
return not Counter(ransomNote) - Counter(magazine)
3.3 性能比较与选择建议
在LeetCode实测中:
- 数组方法:36ms,内存14.1MB
- Counter方法:52ms,内存14.3MB
经验:在只需要处理小写字母时,数组哈希表是最优选择。当字符范围不确定时,使用Counter更安全。
4. 15. 三数之和:双指针法的精妙运用
4.1 问题特殊性与挑战
找出所有不重复的三元组,使其和为0。难点在于:
- 需要去重
- 最优解法需要O(n^2)时间复杂度
- 边界条件较多
4.2 排序+双指针标准解法
python复制def threeSum(nums):
nums.sort()
res = []
n = len(nums)
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, n - 1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total < 0:
left += 1
elif total > 0:
right -= 1
else:
res.append([nums[i], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
return res
4.3 关键细节解析
- 排序是双指针法的前提
- 外层循环固定第一个数,内层双指针寻找另外两个数
- 去重处理:
- 外层循环跳过相同元素
- 找到解后跳过左右指针的相同元素
踩坑记录:忘记处理内层循环的去重是常见错误,会导致结果中出现重复三元组。
5. 18. 四数之和:三数之和的进阶版
5.1 问题扩展思路
在三数之和的基础上增加一层循环,时间复杂度O(n^3)。同样需要:
- 排序预处理
- 双重循环+双指针
- 严格的去重逻辑
5.2 标准实现代码
python复制def fourSum(nums, target):
nums.sort()
res = []
n = len(nums)
for i in range(n - 3):
if i > 0 and nums[i] == nums[i - 1]:
continue
for j in range(i + 1, n - 2):
if j > i + 1 and nums[j] == nums[j - 1]:
continue
left, right = j + 1, n - 1
while left < right:
total = nums[i] + nums[j] + nums[left] + nums[right]
if total < target:
left += 1
elif total > target:
right -= 1
else:
res.append([nums[i], nums[j], nums[left], nums[right]])
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
return res
5.3 优化技巧与注意事项
- 提前终止条件:当最小和大于target或最大和小于target时可提前结束
- 整数溢出问题:在极端情况下,四个大整数相加可能导致溢出
- 剪枝优化:
python复制# 第一层剪枝 if nums[i] * 4 > target: break # 第二层剪枝 if nums[i] + nums[j] * 3 > target: break
6. 算法模板总结与对比
6.1 哈希表与双指针适用场景对比
| 方法 | 适用问题 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|---|
| 哈希表 | 两数之和类 | O(n) | O(n) | 通用性强,需要额外空间 |
| 双指针 | 有序数组多数和 | O(n^k-1) | O(1)或O(n) | 需要排序,节省空间 |
6.2 四道题目的内在联系
- 454题展示了如何用哈希表将O(n^4)优化到O(n^2)
- 383题是哈希表在字符频率统计中的基础应用
- 15题和18题展示了双指针法在有序数组中的高效应用
- 这四道题共同构成了从两数之和到多数之和的完整知识体系
7. 面试实战技巧与常见错误
7.1 面试官常问的follow-up问题
- 如果数组很大无法一次性加载到内存怎么办?
- 如何修改算法处理浮点数?
- 如果要求返回索引而非数值该如何处理?
- 如何优化算法以利用多核CPU?
7.2 白板编程时的注意事项
- 先明确输入输出及边界条件
- 讨论时间/空间复杂度后再编码
- 写出清晰变量名而非单字母
- 主动解释去重等关键步骤
7.3 性能优化进阶思路
- 对于三/四数之和,可预先计算并缓存部分和
- 使用位图替代哈希表节省空间
- 考虑并行化处理不同区间的组合
- 对极大数组可采用分批处理策略
在实际面试中,我通常会先写出标准解法,然后与面试官讨论各种可能的优化方向。这比直接追求最优解更能展示思考过程。对于这类题目,理解其内在联系比死记硬背单个解法更重要。
