1. 算法实战:长度最小的子数组 —— 滑动窗口(双指针)最优解
最近在刷力扣Hot100时遇到了这道经典题目,作为面试高频考点,它完美展现了滑动窗口算法的精妙之处。这道题我前后做了三遍才真正吃透,今天就把我的解题心得完整分享给大家,特别是那些容易忽略的边界条件和优化细节。
2. 题目深度解析
2.1 问题重述与示例分析
给定一个正整数数组nums和目标值target,我们需要找到满足以下条件的最短连续子数组:
- 子数组元素之和 ≥ target
- 在所有满足条件的子数组中长度最小
以示例target=7,nums=[2,3,1,2,4,3]为例:
2.2 暴力解法的时间复杂度陷阱
很多同学的第一反应是使用双重循环暴力枚举:
python复制def minSubArrayLen(target, nums):
min_len = float('inf')
n = len(nums)
for i in range(n):
current_sum = 0
for j in range(i, n):
current_sum += nums[j]
if current_sum >= target:
min_len = min(min_len, j-i+1)
break
return min_len if min_len != float('inf') else 0
这种解法的时间复杂度是O(n²),当n=10⁵时(力扣常见测试用例规模),计算量会达到10¹⁰次操作,远超时间限制(通常要求10⁶-10⁷次操作内完成)。
3. 滑动窗口算法精讲
3.1 算法核心思想
滑动窗口通过维护一个动态变化的窗口来避免重复计算,其核心在于:
- 窗口由左右指针(left, right)定义
- 右指针负责扩展窗口(探索新元素)
- 左指针负责收缩窗口(排除不必要元素)
这种单次遍历的方式将时间复杂度优化到O(n),空间复杂度保持O(1)。
3.2 算法执行流程详解
让我们用示例nums=[2,3,1,2,4,3], target=7逐步演示:
初始化:
- left = 0, sum = 0, min_len = ∞
步骤1:right=0 (元素2)
- sum = 2
- 2 < 7,不更新
步骤2:right=1 (元素3)
- sum = 5
- 5 < 7,不更新
步骤3:right=2 (元素1)
- sum = 6
- 6 < 7,不更新
步骤4:right=3 (元素2)
- sum = 8
- 8 ≥ 7:
- 更新min_len = min(∞, 3-0+1) = 4
- 左移left:sum -= 2 → 6, left=1
- 6 < 7,停止移动
步骤5:right=4 (元素4)
- sum = 10
- 10 ≥ 7:
- 更新min_len = min(4, 4-1+1) = 4
- 左移left:sum -= 3 → 7, left=2
- 7 ≥ 7:
- 更新min_len = min(4, 4-2+1) = 3
- 左移left:sum -= 1 → 6, left=3
- 6 < 7,停止移动
步骤6:right=5 (元素3)
- sum = 9
- 9 ≥ 7:
- 更新min_len = min(3, 5-3+1) = 2
- 左移left:sum -= 2 → 7, left=4
- 7 ≥ 7:
- 更新min_len = min(2, 5-4+1) = 2
- 左移left:sum -= 4 → 3, left=5
- 3 < 7,停止移动
最终结果:min_len = 2
4. 代码实现与优化技巧
4.1 Java最优解实现
java复制class Solution {
public int minSubArrayLen(int target, int[] nums) {
int minLen = Integer.MAX_VALUE;
int left = 0;
int sum = 0;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left++];
// 提前退出优化:最小可能长度是1
if (minLen == 1) return 1;
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
}
4.2 关键实现细节
-
初始值设置:
- minLen初始为Integer.MAX_VALUE(或nums.length+1)
- 这样可以用最后的比较判断是否有解
-
窗口收缩条件:
- 使用while而非if,确保窗口能收缩到最小
-
提前退出优化:
- 当发现minLen=1时可直接返回,因为这是最小可能值
5. 复杂度分析与变种问题
5.1 时间复杂度证明
- 每个元素最多被右指针访问一次
- 每个元素最多被左指针访问一次
- 总操作次数 ≈ 2n → O(n)
5.2 空间复杂度
只使用了固定数量的变量 → O(1)
5.3 相关变种题目
- 乘积小于K的子数组个数
- 最长的无重复字符子串
- 包含所有字符的最短子串
6. 常见错误与调试技巧
6.1 典型错误案例
错误1:使用if代替while收缩窗口
java复制// 错误代码
if (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left++];
}
这样无法保证窗口收缩到最小,可能错过更优解。
错误2:初始minLen设为0
会导致Math.min永远取到0,无法正确更新。
6.2 调试建议
- 打印窗口状态:
java复制System.out.println("left="+left+" right="+right+" sum="+sum+" minLen="+minLen);
- 小规模测试用例:
- nums=[1,1,1,1,1], target=3
- nums=[5,1,3,5,10], target=11
7. 算法扩展思考
7.1 如果数组包含负数?
此时滑动窗口失效,因为:
- 窗口扩展时sum可能减小
- 窗口收缩时sum可能增大
替代方案:前缀和+二分查找(时间复杂度O(nlogn))
7.2 如果要求子数组和等于target?
需要修改判断条件:
java复制while (sum >= target) {
if (sum == target) {
minLen = Math.min(minLen, right - left + 1);
}
sum -= nums[left++];
}
7.3 多指针滑动窗口
对于更复杂的问题(如同时满足多个条件),可能需要使用三指针甚至更多指针来维护多个窗口边界。
在实际面试中,面试官可能会逐步增加题目难度,从基础版本延伸到这些变种。理解滑动窗口的本质思想比死记硬背模板更重要。我建议大家在理解这个算法后,去尝试解决LeetCode上相关的15道滑动窗口题目,这样才能真正掌握这个重要的算法技巧。