1. 算法刷题日课:当DP遇上双指针
今天要啃的8道算法题涵盖了动态规划、双指针、二分查找等经典解题范式。作为过来人,我特别理解新手面对这些题目时的困惑——明明每个算法单独看都懂,组合起来就懵圈。这次我们就用"庖丁解牛"的方式,把每道题的解题脉络理得清清楚楚。
先看题目清单里的明星选手:打家劫舍系列是动态规划的经典入门题,它的变种能考察对状态转移方程的灵活运用;双指针和滑动窗口经常在子数组问题中联手出击;而二分查找看似简单,边界条件却暗藏杀机。下面我会结合自己在大厂面试中遇到的真题案例,带大家拆解这些算法的实战应用技巧。
2. 动态规划专题:打家劫舍的三种姿势
2.1 基础版打家劫舍(LeetCode 198)
题目描述很简单:一排房屋,每个房屋存放特定金额,相邻房屋不能同时被打劫,求最大收益。这就像玩一个策略游戏,每次选择都会影响后续选择。
状态转移方程是这类问题的核心:
python复制dp[i] = max(dp[i-1], dp[i-2] + nums[i])
这里dp[i]表示前i个房屋能获得的最大金额。关键点在于理解为什么是i-2而不是i-1——因为不能连续打劫相邻房屋。
踩坑提醒:初始化时dp[0]=nums[0],但dp[1]需要取max(nums[0], nums[1]),这个边界条件容易被忽略
2.2 环形房屋打劫(LeetCode 213)
升级版把房屋排成环形,意味着首尾也视为相邻。这时候的解题技巧是分两种情况讨论:
- 不打劫最后一间房(计算nums[0:n-1])
- 不打劫第一间房(计算nums[1:n])
然后取两种情况的最大值。这种"问题分解"的思路在动态规划中非常常见。
2.3 二叉树结构打劫(LeetCode 337)
当房屋排列变成二叉树时,我们需要用后序遍历来处理。每个节点返回两个值:
- 偷当前节点的最大收益
- 不偷当前节点的最大收益
python复制def rob(root):
def dfs(node):
if not node: return (0, 0)
left = dfs(node.left)
right = dfs(node.right)
rob = node.val + left[1] + right[1]
not_rob = max(left) + max(right)
return (rob, not_rob)
return max(dfs(root))
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
经验之谈:在链表问题中,经常需要创建dummy节点处理头节点可能被修改的情况
3.2 滑动窗口解决子串问题
最小覆盖子串(LeetCode 76)要求找到包含目标所有字符的最短子串。滑动窗口的解法框架如下:
- 用哈希表记录目标字符需求
- 移动右指针扩展窗口
- 当窗口满足条件时,移动左指针收缩窗口
- 记录最小窗口
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, c in enumerate(s, 1):
if need[c] > 0:
missing -= 1
need[c] -= 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 标准二分查找实现
虽然思想简单,但二分查找的边界条件很容易出错。记住这个万能模板:
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
关键点:
- 循环条件是left <= right
- mid计算使用left + (right - left) // 2防止溢出
- 边界更新是mid ± 1
4.2 旋转排序数组搜索(LeetCode 33)
这是二分查找的变种题,需要先判断哪半边是有序的:
python复制def search(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 303)是前缀和的典型应用。预处理阶段计算前缀和数组,查询时只需做减法:
python复制class NumArray:
def __init__(self, nums):
self.prefix = [0] * (len(nums) + 1)
for i in range(len(nums)):
self.prefix[i+1] = self.prefix[i] + nums[i]
def sumRange(self, left, right):
return self.prefix[right+1] - self.prefix[left]
5.2 机器人路径方向控制
机器人能否返回原点(LeetCode 657)考察方向控制的基本思想。用两个变量记录水平和垂直方向的移动:
python复制def judgeCircle(moves):
x = y = 0
for move in moves:
if move == 'U': y += 1
elif move == 'D': y -= 1
elif move == 'L': x -= 1
elif move == 'R': x += 1
return x == 0 and y == 0
6. 算法组合实战技巧
在实际面试中,经常需要组合多种算法解决问题。比如先用滑动窗口找到满足条件的子数组,再用前缀和计算相关指标。这里分享几个实用技巧:
- 当问题涉及"连续子数组"时,滑动窗口和前缀和是首选
- 遇到"最大/最小值"问题时,考虑动态规划或贪心算法
- 数据有序或可以排序时,二分查找可能派上用场
- 链表问题中,双指针能解决大部分需要定位的问题
调试心得:在纸上画出指针移动过程或DP状态表,比单纯看代码更容易发现逻辑错误
7. 常见错误与调试方法
根据我带新人刷题的经验,这些错误最常见:
- 二分查找的无限循环(边界条件错误)
- 动态规划忘记初始化或状态转移方程错误
- 滑动窗口的指针移动条件不完整
- 递归问题缺少终止条件
调试建议:
- 对于DP问题,打印出整个DP表观察状态变化
- 对于指针问题,在关键步骤打印指针位置和对应元素
- 对于递归问题,缩进打印递归深度和参数
8. 刷题路线进阶建议
如果你想系统提升算法能力,我建议这样安排:
- 先掌握每个算法的标准模板(如本文提到的那些)
- 做20道同类型题目巩固理解
- 尝试用不同方法解决同一道题(比如既用DP又用回溯)
- 参加周赛锻炼实战能力
- 定期复习错题本
最后分享一个真实案例:有位同学坚持每天3道题,半年后从算法小白变成了ACMer。关键不在于刷题数量,而在于每道题都吃透解题思路。就像今天这8道题,如果真能理解其中的算法思想和实现细节,面试中的大部分中等难度问题都能应对自如了。