1. 哈希算法实战精解
哈希表作为算法领域的瑞士军刀,其O(1)时间复杂度的查询特性使其成为解决各类问题的利器。在实际工程和算法面试中,合理运用哈希可以大幅提升程序效率。下面我们通过典型例题深入剖析哈希的应用技巧。
1.1 两数之和的两种解法对比
给定整数数组nums和目标值target,找出和为target的两个数的索引。这个看似简单的问题却蕴含着算法优化的经典思路。
1.1.1 暴力枚举法
python复制class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[i] + nums[j] == target:
return [i, j]
暴力解法通过双重循环检查所有可能的数对组合:
- 时间复杂度:O(n²) —— 对于n个元素,需要检查n(n-1)/2种组合
- 空间复杂度:O(1) —— 仅使用常数空间
实际工程中,当n>10⁴时这种解法就会明显变慢。我在处理大规模数据时曾因使用暴力法导致接口超时,教训深刻。
1.1.2 哈希优化法
python复制class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
hashmap = {}
for i, num in enumerate(nums):
complement = target - num
if complement in hashmap:
return [hashmap[complement], i]
hashmap[num] = i
哈希解法通过空间换时间:
- 时间复杂度:O(n) —— 只需单次遍历
- 空间复杂度:O(n) —— 存储元素到索引的映射
关键技巧:
- 在遍历时动态构建哈希表,避免预处理
- 先检查补数再插入当前数,防止重复使用同一元素
- 存储索引而非值,便于直接返回结果
1.2 字母异位词分组的哈希技巧
字母异位词指字母相同但排列不同的单词,如"eat"和"tea"。分组这类词需要找到统一的哈希键。
1.2.1 排序哈希法
python复制class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
hashmap = {}
for s in strs:
key = "".join(sorted(s))
if key not in hashmap:
hashmap[key] = []
hashmap[key].append(s)
return list(hashmap.values())
算法核心:
- 将每个单词排序后作为哈希键
- 相同键的单词归为同一组
- 时间复杂度:O(nklogk),其中n是单词数,k是最大单词长度
我在实际项目中处理用户搜索词时,曾用此法快速识别近义词。注意Python中字符串排序需先转为list再join,这是常见陷阱。
1.2.2 计数哈希法(优化变种)
对于仅含小写字母的场景,可用计数数组替代排序:
python复制def groupAnagrams(strs):
hashmap = {}
for s in strs:
count = [0] * 26
for c in s:
count[ord(c) - ord('a')] += 1
key = tuple(count)
hashmap.setdefault(key, []).append(s)
return list(hashmap.values())
此方法将时间复杂度降至O(nk),适合长字符串场景。
1.3 最长连续序列的哈希妙用
给定未排序数组,找出数字连续的最长序列长度。要求O(n)时间复杂度。
1.3.1 哈希集合剪枝法
python复制class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
num_set = set(nums)
longest = 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
longest = max(longest, length)
return longest
算法精要:
- 使用集合实现O(1)查询
- 仅当num是序列起点时(即num-1不存在)才开始计数
- 避免重复计算,确保O(n)时间复杂度
我曾用此方法分析用户连续登录天数,相比排序解法性能提升显著。注意Python中集合的in操作平均O(1),但最坏情况O(n),工业级应用需考虑改用布隆过滤器。
2. 双指针算法深度剖析
双指针技术通过维护多个指针协同遍历数据结构,能有效解决许多线性表问题。下面我们解析几个典型应用场景。
2.1 移动零问题的双指针解法
将数组中的零移动到末尾,保持非零元素相对顺序。
2.1.1 非原地操作解法(不推荐)
python复制class Solution:
def moveZeroes(self, nums: List[int]) -> None:
ans = []
zeros = 0
for num in nums:
if num != 0:
ans.append(num)
else:
zeros += 1
ans.extend([0] * zeros)
nums[:] = ans
此解法不符合原地操作要求,且空间复杂度O(n)。
2.1.2 双指针原地操作
python复制class Solution:
def moveZeroes(self, nums: List[int]) -> None:
pos = 0 # 非零元素插入位置
for i in range(len(nums)):
if nums[i] != 0:
nums[pos], nums[i] = nums[i], nums[pos]
pos += 1
优化点:
- pos指针标记下一个非零元素应放的位置
- 遍历过程中交换非零元素到前面
- 时间复杂度O(n),空间复杂度O(1)
实际调试时发现,若直接赋值而非交换,会导致非零元素被覆盖。这是双指针操作中的常见陷阱。
2.2 盛水容器问题的贪心策略
寻找两条垂线,使其与x轴构成的容器能盛最多水。
2.2.1 双指针解法
python复制class Solution:
def maxArea(self, height: List[int]) -> int:
l, r = 0, len(height) - 1
max_area = 0
while l < r:
area = (r - l) * min(height[l], height[r])
max_area = max(max_area, area)
if height[l] < height[r]:
l += 1
else:
r -= 1
return max_area
关键理解:
- 初始时宽度最大,通过移动指针逐步缩小宽度
- 总是移动较短的板,因为移动长板不可能增加面积
- 时间复杂度O(n),优于暴力法的O(n²)
2.3 三数之和的去重技巧
找出数组中所有不重复的三元组,使其和为0。
2.3.1 排序+双指针法
python复制class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
res = []
for i in range(len(nums)):
if i > 0 and nums[i] == nums[i-1]: # 跳过重复元素
continue
l, r = i + 1, len(nums) - 1
while l < r:
s = nums[i] + nums[l] + nums[r]
if s < 0:
l += 1
elif s > 0:
r -= 1
else:
res.append([nums[i], nums[l], nums[r]])
while l < r and nums[l] == nums[l+1]: # 跳过左侧重复
l += 1
while l < r and nums[r] == nums[r-1]: # 跳过右侧重复
r -= 1
l += 1
r -= 1
return res
注意事项:
- 必须先排序才能使用双指针
- 三重去重:外层循环、左指针、右指针
- 时间复杂度O(n²),空间复杂度取决于排序算法
3. 滑动窗口算法精要
滑动窗口是处理子串/子数组问题的利器,通过动态调整窗口边界来高效解决问题。
3.1 无重复字符的最长子串
3.1.1 标准滑动窗口解法
python复制class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
char_set = set()
left = 0
max_len = 0
for right in range(len(s)):
while s[right] in char_set: # 收缩左边界
char_set.remove(s[left])
left += 1
char_set.add(s[right])
max_len = max(max_len, right - left + 1)
return max_len
算法特点:
- 使用集合记录当前窗口字符
- 遇到重复字符时收缩左边界
- 时间复杂度O(n),每个字符最多被访问两次
3.2 字符串异位词查找
在字符串s中找出所有p的异位词的起始索引。
3.2.1 滑动窗口+计数器
python复制class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
if len(s) < len(p):
return []
p_count = Counter(p)
window = Counter()
res = []
for i in range(len(s)):
window[s[i]] += 1
if i >= len(p):
left_char = s[i - len(p)]
if window[left_char] == 1:
del window[left_char]
else:
window[left_char] -= 1
if window == p_count:
res.append(i - len(p) + 1)
return res
关键点:
- 使用Counter比较窗口与目标字符频率
- 窗口大小固定为p的长度
- 每次滑动时更新窗口计数器
4. 子串问题进阶技巧
4.1 和为K的子数组
统计数组中连续子数组和为k的个数。
4.1.1 前缀和+哈希表
python复制class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
prefix_sum = {0: 1}
current_sum = 0
count = 0
for num in nums:
current_sum += num
count += prefix_sum.get(current_sum - k, 0)
prefix_sum[current_sum] = prefix_sum.get(current_sum, 0) + 1
return count
核心思想:
- 前缀和presum[j] - presum[i] = k表示子数组i+1到j的和为k
- 用哈希表记录各前缀和出现次数
- 时间复杂度O(n),空间复杂度O(n)
4.2 滑动窗口最大值
4.2.1 单调队列解法
python复制from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
dq = deque()
res = []
for i in range(len(nums)):
while dq and dq[0] < i - k + 1: # 移除窗口外的索引
dq.popleft()
while dq and nums[dq[-1]] < nums[i]: # 移除小于当前值的元素
dq.pop()
dq.append(i)
if i >= k - 1:
res.append(nums[dq[0]])
return res
单调队列特性:
- 队列元素按从大到小排列
- 队首始终是当前窗口最大值
- 每个元素最多入队出队各一次,时间复杂度O(n)
5. 普通数组问题的经典解法
5.1 最大子数组和
5.1.1 动态规划解法
python复制class Solution:
def maxSubArray(self, nums: List[int]) -> int:
current_max = global_max = nums[0]
for num in nums[1:]:
current_max = max(num, current_max + num)
global_max = max(global_max, current_max)
return global_max
DP思想:
- current_max表示以当前元素结尾的最大和
- 若前序和为负则从当前元素重新开始
- 时间复杂度O(n),空间复杂度O(1)
5.2 合并区间
5.2.1 排序+线性扫描
python复制class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x: x[0])
merged = []
for interval in intervals:
if not merged or merged[-1][1] < interval[0]:
merged.append(interval)
else:
merged[-1][1] = max(merged[-1][1], interval[1])
return merged
关键步骤:
- 按区间起点排序
- 逐个合并重叠区间
- 时间复杂度O(nlogn),主要来自排序
5.3 缺失的第一个正数
5.3.1 原地哈希法
python复制class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
for i in range(n):
while 1 <= nums[i] <= n and nums[i] != nums[nums[i] - 1]:
nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
for i in range(n):
if nums[i] != i + 1:
return i + 1
return n + 1
算法精髓:
- 利用数组本身作为哈希表
- 将数字i放到i-1的位置
- 时间复杂度O(n),空间复杂度O(1)