一、开篇:你是否也被“子数组”题目难住了?
刷LeetCode时,遇到“长度最小的子数组”这类题目,你是不是也有过这样的疑惑:
- “到底怎么才能又快又准地找到最短的子数组?”
- “暴力法超时怎么办?”
- “滑动窗口到底怎么用?”
别急,今天我就用最通俗的语言,带你彻底搞懂这道题!
二、题目简介
LeetCode 209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target,找出该数组中满足其和 ≥ target 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入: target = 7, nums = [2,3,1,2,4,3]
输出: 2
解释: 子数组 [4,3] 是该条件下的长度最小的子数组。
三、暴力法:新手的第一步
最直接的想法是什么?当然是“把所有可能的子数组都试一遍”!
代码实现
function minSubArrayLen(target, nums) {
let minLen = Infinity;
for (let i = 0; i < nums.length; i++) {
let sum = 0;
for (let j = i; j < nums.length; j++) {
sum += nums[j];
if (sum >= target) {
minLen = Math.min(minLen, j - i + 1);
break; // 已经满足条件,没必要再往后加了
}
}
}
return minLen === Infinity ? 0 : minLen;
}
复杂度分析
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
缺点:当数组很大时,暴力法会超时!
四、滑动窗口:高效的“窗口”思维
滑动窗口是什么?
滑动窗口就像一扇可以左右移动的窗户,我们用两个指针(left和right)来标记窗口的左右边界。窗口内的元素之和如果小于target,就扩大窗口(右移right);如果大于等于target,就缩小窗口(右移left),同时记录最小长度。
代码实现
function minSubArrayLen(target, nums) {
let left = 0, sum = 0, minLen = Infinity;
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLen === Infinity ? 0 : minLen;
}
过程详解
- 初始化:left=0,sum=0,minLen=Infinity
- 右指针扩展窗口:每次加上nums[right]
- 窗口内和满足条件:sum >= target
- 记录当前窗口长度
- 尝试缩小窗口(left右移),并更新sum
- 循环结束:返回minLen
动图演示
假设 target=7, nums=[2,3,1,2,4,3]
- right=0, sum=2
- right=1, sum=5
- right=2, sum=6
- right=3, sum=8(满足条件,窗口长度4,尝试缩小)
- left=1, sum=6(不满足,继续右移right)
- right=4, sum=10(满足条件,窗口长度4,继续缩小)
- left=2, sum=9(满足条件,窗口长度3,继续缩小)
- left=3, sum=8(满足条件,窗口长度2,继续缩小)
- left=4, sum=4(不满足,继续右移right)
- right=5, sum=7(满足条件,窗口长度2)
最终最小长度为2。
五、代码优化与细节
- 为什么用while循环?
因为每次sum>=target时,我们都要尽可能缩小窗口,找到最短的长度。 - 为什么要判断minLen是否为Infinity?
如果没有任何子数组满足条件,应该返回0。
六、进阶思考
- 如果数组中有负数怎么办?
滑动窗口法只适用于全正数数组。如果有负数,问题会变复杂,需要用其他算法。 - 还能不能再优化?
目前的滑动窗口法已经是O(n)最优解了。
七、总结
- 暴力法适合理解题意,但效率低
- 滑动窗口法高效且易于实现,是解决“连续子数组”问题的利器
- 多画图、多调试,滑动窗口思想会越来越熟练!
八、附录:完整代码
// 暴力法
function minSubArrayLen1(target, nums) {
let minLen = Infinity;
for (let i = 0; i < nums.length; i++) {
let sum = 0;
for (let j = i; j < nums.length; j++) {
sum += nums[j];
if (sum >= target) {
minLen = Math.min(minLen, j - i + 1);
break;
}
}
}
return minLen === Infinity ? 0 : minLen;
}
// 滑动窗口法
function minSubArrayLen(target, nums) {
let left = 0, sum = 0, minLen = Infinity;
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLen === Infinity ? 0 : minLen;
}
九、写在最后
作为初学者,刚开始接触滑动窗口时可能会觉得抽象,但只要多练习几道类似的题目,你一定会发现它的强大。希望这篇文章能帮你扫清“长度最小的子数组”这道题的所有疑惑!