在数组中找大于等于目标值的最小子数组的长度
LeetCode 209 · 滑动窗口经典题型
题目描述
给定一个含有 n 个正整数的数组和一个正整数 target。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr],并返回其长度。如果不存在符合条件的子数组,返回 0。
解题思路
核心思路:用滑动窗口找到以每个元素开头的最小子数组,不断更新最小长度。
第一步:初始化最小长度
javascript
let min = nums.length + 1;
为什么让 min = nums.length + 1 而不是 nums.length?
因为题目要求:不存在符合条件的子数组时,返回 0。
假设令 min = nums.length,来看一个问题场景:
| 步骤 | 操作 | sum | min |
|---|---|---|---|
| 1 | 遍历完整个数组 | sum < target | nums.length(未更新) |
遍历结束后 sum 仍然小于 target,说明不存在符合条件的子数组,应该返回 0。
但此时 min = nums.length,如果直接返回 min,就返回了错误的值。
解决方案:令 min = nums.length + 1,用它作为"未找到"的标志。
最终返回时:
javascript
return min === nums.length + 1 ? 0 : min;
这样只要 min 没被更新过(还是初始值),就说明没找到,返回 0。
第二步:滑动窗口找最小长度
用两个指针 left 和 right 维护一个窗口:
left:确定子数组的开头right:确定子数组的结尾,不断右移使sum ≥ target
javascript
let left = 0;
let right = 0;
let sum = 0;
let min = nums.length + 1;
for (; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
min = Math.min(right - left + 1, min);
sum -= nums[left];
left++;
}
}
return min === nums.length + 1 ? 0 : min;
关键问题:为什么用 while 而不是 if?
javascript
while (sum >= target) { // ← 为什么是 while,不是 if?
min = Math.min(right - left + 1, min);
sum -= nums[left];
left++;
}
因为当当前子数组的和 sum ≥ target 时,就可以开始尝试缩短子数组,看看去掉左边元素后是否仍然满足条件。
如果用 if:只会缩短一次就跳出,可能错过更短的合法子数组。
用 while:会一直缩短,直到 sum < target,确保找到的是以当前 right 结尾的最短合法子数组。
图解示例
以 nums = [2, 3, 1, 2, 4, 3],target = 7 为例:
| 步骤 | 窗口 | sum | 操作 |
|---|---|---|---|
| 1 | [2] |
2 | right 右移 |
| 2 | [2, 3] |
5 | right 右移 |
| 3 | [2, 3, 1] |
6 | right 右移 |
| 4 | [2, 3, 1, 2] |
8 ≥ 7 | min = 4,left 右移 → sum = 6 < 7 |
| 5 | [3, 1, 2, 4] |
10 ≥ 7 | min = 4,left 右移 → [1, 2, 4] sum = 7 ≥ 7,min = 3 ,left 右移 → [2, 4] sum = 6 < 7 |
| 6 | [2, 4, 3] |
9 ≥ 7 | min = 3,left 右移 → [4, 3] sum = 7 ≥ 7,min = 2 ,left 右移 → [3] sum = 3 < 7 |
最终结果:min = 2(子数组 [4, 3])
注意步骤 5 :
while让窗口从[3, 1, 2, 4]逐步缩短到[2, 4],中间经过[1, 2, 4](sum = 7 ≥ 7),所以min能被更新为 3。如果用if就会错过这一步。
完整代码
javascript
var minSubArrayLen = function (target, nums) {
let left = 0;
let sum = 0;
let min = nums.length + 1;
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
min = Math.min(right - left + 1, min);
sum -= nums[left];
left++;
}
}
return min === nums.length + 1 ? 0 : min;
};
复杂度分析
| 复杂度 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 每个元素最多被 left 和 right 各访问一次 |
| 空间复杂度 | O(1) | 只用了几个变量 |
总结
| 要点 | 说明 |
|---|---|
min 初始化为 nums.length + 1 |
作为"未找到"的标志,最终通过三元表达式返回 0 或 min |
while 而非 if |
确保找到以当前 right 结尾的最短合法子数组 |
窗口缩小时更新 min |
每次 left 右移前,用当前窗口长度尝试更新最小值 |