1. 算法刷题Day04:三道经典题目的深度剖析
今天继续我们的算法刷题之旅,这是代码随想录系列的第4天。我们将重点攻克三道LeetCode经典题目:904.水果成篮、76.最小覆盖子串和59.螺旋矩阵Ⅱ。这三道题分别代表了滑动窗口和模拟操作两大重要算法题型,在面试中出现频率极高。
我在实际刷题过程中发现,很多同学对滑动窗口类问题的理解往往停留在表面,而螺旋矩阵这类模拟题又容易陷入"写起来简单但调试困难"的困境。本文将结合我多次面试和被面试的经验,带你看透这三道题的本质,并分享一些调试技巧和优化思路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 904.水果成篮 - 滑动窗口的典型应用
2.1 题目重述与初步分析
题目描述:你正在采摘水果,果园里有一排果树,每棵果树上有一种类型的水果,用整数表示。你只有两个篮子,每个篮子只能装一种类型的水果(但两个篮子可以装相同类型)。采摘需要从任意一棵树开始,按顺序采摘,每采摘一棵树就会把水果放进对应的篮子。如果当前树上的水果无法放入任何篮子(即出现了第三种类型),采摘就会停止。求最多可以采摘多少棵树。
示例:
输入:[3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘[1,2,1,1,2],共5棵树
这道题本质上是在寻找最长的连续子数组,其中包含不超过两种不同的数字。这明显是一个滑动窗口问题。
2.2 滑动窗口解法详解
滑动窗口算法的核心在于维护一个窗口,通过调整窗口的左右边界来寻找符合条件的子数组。对于本题,我们需要:
- 使用哈希表(或字典)来记录当前窗口中各水果类型的出现次数
- 当哈希表大小超过2时,移动左边界缩小窗口
- 在每一步更新最大窗口大小
python复制def totalFruit(fruits):
from collections import defaultdict
count = defaultdict(int)
left = res = 0
for right, fruit in enumerate(fruits):
count[fruit] += 1
while len(count) > 2:
left_fruit = fruits[left]
count[left_fruit] -= 1
if count[left_fruit] == 0:
del count[left_fruit]
left += 1
res = max(res, right - left + 1)
return res
注意:这里使用defaultdict是为了避免处理键不存在的异常。在实际面试中,使用普通字典并手动处理也是可以的。
2.3 优化思路与常见错误
一个常见的优化是记录最后一次出现的位置而不是计数:
python复制def totalFruit(fruits):
last_index = {}
left = res = 0
for right, fruit in enumerate(fruits):
last_index[fruit] = right
if len(last_index) > 2:
left = min(last_index.values()) + 1
del last_index[fruits[left-1]]
res = max(res, right - left + 1)
return res
这种优化将时间复杂度从O(2n)降低到O(n),因为每个元素最多被处理两次(加入和删除)。
常见错误包括:
- 没有及时删除计数为0的键,导致len(count)计算错误
- 在移动左边界时没有逐步移动,而是一次跳过多位
- 初始化时没有考虑空输入的情况
3. 76.最小覆盖子串 - 滑动窗口的进阶应用
3.1 问题描述与难点分析
题目要求:给定字符串S和T,在S中找到包含T所有字符的最短子串。
示例:
输入:S = "ADOBECODEBANC", T = "ABC"
输出:"BANC"
这道题比水果成篮更复杂,因为:
- 需要包含所有字符,而不仅仅是种类数限制
- 字符可能有重复
- 需要考虑字符的出现次数
3.2 滑动窗口解法实现
我们需要两个哈希表:一个记录T中字符的需求量,一个记录当前窗口中的字符统计。
python复制def minWindow(s: str, t: str) -> str:
from collections import defaultdict
need = defaultdict(int)
for c in t:
need[c] += 1
needCnt = len(t)
left = 0
res = (0, float('inf'))
for right, c in enumerate(s):
if need[c] > 0:
needCnt -= 1
need[c] -= 1
if needCnt == 0: # 窗口满足条件
while True: # 移动左指针找最小窗口
c = s[left]
if need[c] == 0: # 不能再移动了
break
need[c] += 1
left += 1
if right - left < res[1] - res[0]:
res = (left, right)
# 移动左指针,寻找下一个窗口
need[s[left]] += 1
needCnt += 1
left += 1
return s[res[0]:res[1]+1] if res[1] < float('inf') else ""
3.3 关键点解析与调试技巧
- needCnt的使用很关键,它表示还缺少多少个字符才能满足条件
- need字典中的值:
- 正数:还需要多少个该字符
- 零:刚好满足
- 负数:该字符在窗口中多余的数量
调试技巧:
- 打印窗口变化过程:在每次窗口变动时打印left, right和当前窗口内容
- 使用小例子手动模拟算法执行过程
- 特别注意边界条件:T比S长、T为空、S中包含T没有的字符等情况
4. 59.螺旋矩阵Ⅱ - 模拟操作的经典题目
4.1 问题描述与解题思路
题目要求:给定正整数n,生成一个包含1到n²所有元素的螺旋矩阵。
示例:
输入:n = 3
输出:
[
[1, 2, 3],
[8, 9, 4],
[7, 6, 5]
]
这类模拟题的关键在于:
- 确定填充的顺序和方向
- 处理边界条件和转向时机
- 确保不重复填充已填的位置
4.2 分层填充法实现
我们可以把矩阵看作一层层的同心正方形,从外向内逐层填充:
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 i in range(left, right+1):
matrix[top][i] = num
num += 1
top += 1
# 从上到下填充右边
for i in range(top, bottom+1):
matrix[i][right] = num
num += 1
right -= 1
# 从右到左填充下边
for i in range(right, left-1, -1):
matrix[bottom][i] = num
num += 1
bottom -= 1
# 从下到上填充左边
for i in range(bottom, top-1, -1):
matrix[i][left] = num
num += 1
left += 1
return matrix
4.3 常见错误与调试建议
常见错误包括:
- 边界条件处理不当,导致数组越界
- 转向时机不对,造成重复填充或漏填
- 奇数n的中心点处理不当
调试建议:
- 对于n=1,2,3等小规模情况手动验证
- 在每次方向变化时打印当前矩阵状态
- 使用不同颜色标记不同方向的填充过程
5. 三道题目的共性与差异
虽然这三道题看似不同,但它们都考察了对复杂流程的控制能力:
-
滑动窗口类(904和76):
- 都需要维护某种状态(水果类型或字符计数)
- 都需要动态调整窗口边界
- 区别在于条件判断的复杂度
-
模拟操作类(59):
- 需要精确控制操作顺序和边界
- 通常需要处理多层/多轮操作
- 调试难度通常大于算法难度
在实际面试中,面试官可能会先问904这样的基础滑动窗口题,然后升级到76这样的变种。而螺旋矩阵这类题目则常用来考察候选人的代码实现能力和边界条件处理。
6. 刷题经验与面试技巧
6.1 滑动窗口问题的解题模板
根据我的经验,滑动窗口问题通常可以套用以下模板:
python复制def slidingWindow(s):
# 初始化窗口边界和结果
left = right = 0
res = 0
# 初始化状态记录(通常是哈希表)
window = {}
while right < len(s):
# 扩大窗口,更新状态
c = s[right]
window[c] = window.get(c, 0) + 1
right += 1
while (窗口需要收缩的条件):
# 更新结果(如果需要)
res = ...
# 收缩窗口,更新状态
d = s[left]
window[d] -= 1
if window[d] == 0:
del window[d]
left += 1
return res
6.2 模拟类问题的调试技巧
对于螺旋矩阵这类问题,我的调试经验是:
- 先在小规模(n=1,2,3)上手动模拟,确保基础情况正确
- 添加详细的打印语句,跟踪填充顺序和数值变化
- 使用可视化工具(如简单的矩阵打印)观察填充过程
- 特别注意边界条件和循环终止条件
6.3 面试中的表现建议
在面试中遇到这类题目时:
- 先明确问题要求,给出简单例子确认理解正确
- 从暴力解法开始,逐步优化
- 解释清楚算法思路后再开始编码
- 编码时注意变量命名和代码结构
- 完成后用小例子验证代码正确性
我在面试候选人时,最看重的是思考过程而非直接给出最优解。能够清晰地解释自己的思路,并逐步优化方案的候选人通常能获得更高的评价。
