哈希表是算法面试中的常客,也是解决特定问题的利器。今天我想分享几个经典哈希表问题的解法,包括四数相加II、赎金信、三数之和和四数之和。这些题目看似简单,但其中蕴含着许多值得深思的细节和优化技巧。
四数相加II的问题描述很简单:给定四个整数数组nums1、nums2、nums3、nums4,计算有多少个元组(i,j,k,l)满足nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0。
最直观的暴力解法是四重循环,时间复杂度为O(n^4),这在n=200时就已经达到了1,600,000,000次运算,显然不可行。
我们可以将问题拆分为两部分:
python复制class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
record = dict()
count = 0
for i in nums1:
for j in nums2:
sum_ij = i + j
record[sum_ij] = record.get(sum_ij, 0) + 1
for k in nums3:
for q in nums4:
target = -(k + q)
if target in record:
count += record[target]
return count
时间复杂度:O(n^2),因为我们有两组双重循环
空间复杂度:O(n^2),最坏情况下nums1和nums2的所有和都不相同
提示:在实际面试中,解释清楚为什么选择这种分治策略很重要。哈希表在这里的作用是"记忆"前两个数组的和,避免重复计算。
赎金信问题要求判断杂志(magazine)中的字符是否可以构成赎金信(ransomNote),注意是字符的数量也要足够。
python复制def canConstruct(self, ransomNote: str, magazine: str) -> bool:
for char in ransomNote:
if char in magazine and ransomNote.count(char) <= magazine.count(char):
continue
else:
return False
return True
这种方法简单直接,但效率不高,因为count()方法的时间复杂度是O(n)。
python复制def canConstruct(self, ransomNote: str, magazine: str) -> bool:
ransom_count = [0] * 26
magazine_count = [0] * 26
for c in ransomNote:
ransom_count[ord(c) - ord('a')] += 1
for c in magazine:
magazine_count[ord(c) - ord('a')] += 1
return all(ransom_count[i] <= magazine_count[i] for i in range(26))
这种方法利用了题目中"只有小写字母"的条件,使用固定大小的数组来统计字符出现次数,效率更高。
python复制def canConstruct(self, ransomNote: str, magazine: str) -> bool:
from collections import defaultdict
magazine_dict = defaultdict(int)
for c in magazine:
magazine_dict[c] += 1
for c in ransomNote:
if magazine_dict[c] <= 0:
return False
magazine_dict[c] -= 1
return True
这个版本只需要遍历magazine一次和ransomNote一次,是最优解。
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 直接统计 | O(n*m) | O(1) | 简单实现,不推荐 |
| 数组统计 | O(n+m) | O(1) | 字符集固定且小 |
| 哈希表 | O(n+m) | O(m) | 通用解法 |
注意:在实际编码中,如果知道字符集大小固定且不大(如只有小写字母),数组统计法是最佳选择。
三数之和要求找出所有不重复的三元组,使得它们的和为0。这个问题的难点在于:
python复制class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
result = []
nums.sort()
for i in range(len(nums)):
if nums[i] > 0: # 剪枝优化
return result
if i > 0 and nums[i] == nums[i-1]: # 去重
continue
left, right = i+1, len(nums)-1
while left < right:
total = nums[i] + nums[left] + nums[right]
if total > 0:
right -= 1
elif total < 0:
left += 1
else:
result.append([nums[i], nums[left], nums[right]])
# 去重
while left < right and nums[right] == nums[right-1]:
right -= 1
while left < right and nums[left] == nums[left+1]:
left += 1
right -= 1
left += 1
return result
经验分享:去重的时机很重要,必须在找到一个有效三元组后再进行去重操作,否则可能会漏解。
四数之和是三数之和的扩展,主要区别在于:
python复制class Solution:
def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
result = []
nums.sort()
for k in range(len(nums)):
if nums[k] > target and nums[k] > 0: # 剪枝
break
if k > 0 and nums[k] == nums[k-1]: # 去重
continue
for i in range(k+1, len(nums)):
if nums[k] + nums[i] > target and nums[i] > 0: # 剪枝
break
if i > k+1 and nums[i] == nums[i-1]: # 去重
continue
left, right = i+1, len(nums)-1
while left < right:
total = nums[k] + nums[i] + nums[left] + nums[right]
if total > target:
right -= 1
elif total < target:
left += 1
else:
result.append([nums[k], nums[i], nums[left], nums[right]])
# 去重
while left < right and nums[right] == nums[right-1]:
right -= 1
while left < right and nums[left] == nums[left+1]:
left += 1
right -= 1
left += 1
return result
在实际问题中,如何选择哈希表还是双指针?
| 考虑因素 | 哈希表 | 双指针 |
|---|---|---|
| 时间复杂度 | 通常O(n)或O(n^2) | 通常O(nlogn)或O(n^2) |
| 空间复杂度 | 需要额外空间 | 通常O(1) |
| 适用场景 | 需要快速查找 | 已排序数组 |
| 去重难度 | 较难 | 较易 |
| 实现难度 | 简单 | 中等 |
对于需要返回索引的问题(如两数之和),哈希表是更好的选择;对于需要返回具体数值且可能重复的问题(如三数之和),双指针更合适。
调试技巧:打印中间哈希表的内容,确认统计是否正确
调试技巧:对于小规模输入手动模拟双指针移动过程
这些算法不仅仅是面试题,在实际开发中也有广泛应用:
例如,在电商系统中,可能需要找出价格组合等于优惠券面额的商品组合,这就类似于三数之和问题。
这些问题可以帮助你更深入地理解算法的本质和应用边界。
在解决这些问题时,我最大的体会是:理解问题本质比记忆解法更重要。哈希表和双指针都是工具,关键在于分析问题的特点,选择最适合的工具组合。同时,边界条件和去重逻辑往往是容易出错的地方,需要特别小心。