1. 算法刷题日课:从打家劫舍到滑动窗口的实战精要
今天要啃的八道算法题涵盖了动态规划、双指针、二分查找等经典解题范式。作为过来人,我深知这些题目在面试中的高频出现率——去年帮团队筛选简历时,约70%的候选人都在动态规划问题上栽了跟头。下面就用工程思维拆解这些题目,重点分享那些刷题网站不会告诉你的实战技巧。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动态规划专题:打家劫舍问题深度剖析
2.1 问题建模与状态定义
打家劫舍系列问题本质上是带约束条件的序列最优化问题。以经典版本为例:给定非负整数数组nums表示每个房屋的金额,不能偷窃相邻房屋,求最大收益。
关键突破点在于状态定义:
- dp[i]:考虑前i个房屋时的最大收益
- 状态转移方程:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
实际编码时建议用滚动数组优化空间复杂度到O(1),这在处理长序列时尤为重要。我在处理电商风控系统时,就遇到过需要处理百万级序列的类似场景。
2.2 边界条件处理的艺术
新手常犯的错误是忽略边界条件。对于打家劫舍问题:
- 空数组情况:直接返回0
- 单元素数组:返回nums[0]
- 双元素数组:max(nums[0], nums[1])
python复制def rob(nums):
if not nums: return 0
prev, curr = 0, nums[0]
for num in nums[1:]:
prev, curr = curr, max(curr, prev + num)
return curr
2.3 变种问题实战技巧
环形排列版本(LeetCode 213)的解法:
- 拆解成两个子问题:不偷第一间或不偷最后一间
- 分别求解后取最大值
python复制def rob_circle(nums):
def helper(sub_nums):
# 标准打家劫舍解法
pass
return max(helper(nums[:-1]), helper(nums[1:])) if len(nums) != 1 else nums[0]
树形版本(LeetCode 337)需要后序遍历:
- 每个节点返回[不抢当前节点的最大值, 抢当前节点的最大值]
- 自底向上计算
3. 双指针技术的工程实践
3.1 快慢指针的隐藏陷阱
在处理链表环检测(LeetCode 141)时:
python复制def hasCycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
常见坑点:
- 初始条件检查:head为None或单节点情况
- 移动顺序:必须先移动再比较,否则初始状态就会触发返回
- 终止条件:fast和fast.next都需要判断
3.2 滑动窗口的优化空间
最小覆盖子串问题(LeetCode 76)的优化方案:
- 使用need字典记录目标字符需求
- 用missing计数器避免频繁检查字典
- 收缩左边界时注意字符类型判断
python复制def minWindow(s, t):
from collections import defaultdict
need = defaultdict(int)
for c in t: need[c] += 1
missing = len(t)
left = start = end = 0
for right, char in enumerate(s, 1):
if need[char] > 0:
missing -= 1
need[char] -= 1
if missing == 0:
while left < right and need[s[left]] < 0:
need[s[left]] += 1
left += 1
if not end or right - left <= end - start:
start, end = left, right
return s[start:end]
4. 二分查找的工业级实现
4.1 终止条件的魔鬼细节
标准二分查找有约20%的概率会写出死循环。正确写法:
python复制def binary_search(nums, target):
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
关键点:
- 循环条件用<=而不是<
- mid计算方式防止整数溢出
- 边界更新必须±1
4.2 旋转数组搜索实战
搜索旋转排序数组(LeetCode 33)需要先判断哪边有序:
python复制def search_rotated(nums, target):
left, right = 0, len(nums)-1
while left <= right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
# 左半部分有序
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
5. 方向控制问题的状态机解法
5.1 机器人行走路径问题
LeetCode 874模拟机器人行走时:
- 用方向数组表示北、东、南、西
- 用mod运算处理方向切换
- 障碍物用集合存储提升查询效率
python复制def robotSim(commands, obstacles):
dx = [0, 1, 0, -1]
dy = [1, 0, -1, 0]
obstacles_set = set(map(tuple, obstacles))
x = y = di = 0
max_dist = 0
for cmd in commands:
if cmd == -2:
di = (di - 1) % 4
elif cmd == -1:
di = (di + 1) % 4
else:
for _ in range(cmd):
if (x + dx[di], y + dy[di]) not in obstacles_set:
x += dx[di]
y += dy[di]
max_dist = max(max_dist, x*x + y*y)
return max_dist
5.2 螺旋矩阵的层处理技巧
生成螺旋矩阵(LeetCode 59)时采用分层处理:
- 定义当前层的边界left, right, top, bottom
- 按顺序处理上、右、下、左边
- 每完成一层收缩边界
python复制def generateMatrix(n):
matrix = [[0]*n for _ in range(n)]
left, right, top, bottom = 0, n-1, 0, n-1
num = 1
while left <= right and top <= bottom:
for j in range(left, right+1):
matrix[top][j] = num
num += 1
top += 1
for i in range(top, bottom+1):
matrix[i][right] = num
num += 1
right -= 1
for j in range(right, left-1, -1):
matrix[bottom][j] = num
num += 1
bottom -= 1
for i in range(bottom, top-1, -1):
matrix[i][left] = num
num += 1
left += 1
return matrix
6. 前缀和的实际工程应用
6.1 子数组和问题的通用解法
和为K的子数组(LeetCode 560)的优化解:
- 用字典记录前缀和出现次数
- 边遍历边查询历史记录
python复制def subarraySum(nums, k):
from collections import defaultdict
prefix_sum = defaultdict(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] += 1
return count
6.2 二维前缀和的预处理
处理矩阵块求和时(LeetCode 304),预处理是关键:
python复制class NumMatrix:
def __init__(self, matrix):
if not matrix or not matrix[0]:
self.dp = None
return
m, n = len(matrix), len(matrix[0])
self.dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(m):
for j in range(n):
self.dp[i+1][j+1] = matrix[i][j] + self.dp[i][j+1] + self.dp[i+1][j] - self.dp[i][j]
def sumRegion(self, row1, col1, row2, col2):
if not self.dp:
return 0
return self.dp[row2+1][col2+1] - self.dp[row1][col2+1] - self.dp[row2+1][col1] + self.dp[row1][col1]
7. 算法优化中的常见性能陷阱
- 缓存局部性:在实现二维DP时,按行遍历通常比按列遍历快2-3倍
- 早期剪枝:在回溯问题中,尽早判断无效路径能大幅提升性能
- 数据结构选择:当查询频率远高于插入时,数组比哈希表更高效
- 数学优化:有些问题可以通过数学推导降维(如nSum问题)
8. 面试实战建议
- 先明确问题边界条件和输入范围
- 用具体例子验证思路正确性
- 先给出暴力解再逐步优化
- 讨论时间和空间复杂度的权衡
- 准备测试用例:空输入、极值、正常案例
我在技术面试中最看重候选人的不是最终答案,而是解题过程中的思考逻辑和沟通能力。曾经有位候选人虽然没完全解出打家劫舍变种题,但他清晰地分析了状态转移的各种可能性,这种系统化思维最终让他拿到了offer。
