代码随想录
https://programmercarl.com/0209.长度最小的子数组.html
题目
209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
提示:
1 <= target <= 10^9
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^5
笔记
暴力解法
双重for循环,遍历所有子数组,找出符合条件的子数组里长度最小的。
时间复杂度O(n^2)。
视窗法
实际编写视窗法的三个易错点:
- 何时移动起始位置?
- 何时移动终止位置?
- 终止位置有需要左移的情况吗?
看了一下视窗法的基本原理后自己写了一下:
var minSubArrayLen = function(target, nums) {
// 视窗的左右边缘所在处的索引(对区间的定义是左闭右闭)
let start = 0, end = 0;
// 总和大于等于 target 的长度最小的子数组的长度,初始值设为正无穷
let minLen = Infinity;
// 视窗内元素和
let sum = nums[0];
while (end < nums.length) {
if (sum >= target) {
// 当视窗内元素和大于等于target,检查是否要更新最小子数组长度
minLen = Math.min(minLen, end - start + 1);
// 视窗左边缘往右挪一位,并把刚刚踢出去的元素算入总和
sum -= nums[start];
start ++;
} else {
// 视窗右边缘往右挪一位,并把新加入的元素算入总和
++ end;
sum += nums[end];
}
}
return minLen === Infinity ? 0 : minLen;
};
我第一次自己写时犯过如下错误:
- 我是每次移动视窗边缘后,重头计算一次视窗里所有数的总和,导致时间复杂度直接提升到
O(n^2)
了。其实视窗每次只移动一位,可以每次在原来的总和基础上加上一个进入视窗的数,或减少一个退出视窗的数。 - 我设置了最小子数组长度minLen的初始值为0,导致没有新长度比初始值小。我每次找到新长度就拿来和minLen比较,把更小的话赋值给minLength,但是根本没有新长度比0小。
- 方案1:minLen初始值不要设为数字,最后返回时判断一下minLen有没有变成数字,比如JavaScript语言中的
Infinity
属性,表示无穷数分为正无穷(Infinity
)和负无穷(-Infinity
)。 - 方案2:两数比较时,对
minLen===0
的情况特殊处理一下。
- 方案1:minLen初始值不要设为数字,最后返回时判断一下minLen有没有变成数字,比如JavaScript语言中的
- 每次找到视窗内元素之和大于等于target时,下一步左边缘肯定要往右移一位,元素之和才会下降,那右边缘要不要动呢?
- 我本来想的是右边缘应该往左收缩,缩到左边源旁边,以免遗漏一些左边源移动后的视窗。
- 但是看解析里写的是左边缘右移一位,右边缘不动。想了一下是应该如此,会被遗漏的那些情况本身很明显是元素之和小于target的,没必要考虑。
if (sum < target)
无法保证start <= end
能得到正确的处理,比如计算minSubArrayLen(5, [1,100])
时,start和end的移动轨迹是[0,0]、[0,1]、[1,1]、[2,1],start会取到2,超过数组最大索引,然后sum = sum - nums[2] = NaN
,于是sum < target === false
,代码走到else里记录minLen去了。所以应该用if (sum >= target)
,这样若sum取到NaN,不但影响到minLen,还会走到else方法体中,操作end加一,使end也超过数组边界,停止while循化。- 一开始视窗内元素和sum不等于0,应等于arr[0],因为此时视窗里的元素包含一个arr[0]。
代码随想录里的解答如下:
var minSubArrayLen = function(target, nums) {
let start, end
start = end = 0
let sum = 0
let len = nums.length
let ans = Infinity
while(end < len){
sum += nums[end];
while (sum >= target) {
ans = Math.min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
return ans === Infinity ? 0 : ans
};
也就是双重while循环:
首先左边界start在0处,右边界end从0开始右移,直到end超出数组。每次【右移end】后,要计算加上新进入视窗的元素后的总和sum,若符合sum >= target
则【记录最小子数组长度】再【右移start】。每次【右移start】后,还要再计算sum并记录长度,直到不符合sum >= target
。不符合的情况包括target普通地小于target,start超过end导致元素和为0,end到了数组尾部且start超过了数组尾部导致元素和为NaN
。
即外层循环的继续条件是end < nums.length
,内层循环的继续条件是sum >= target
。
我是单层while循环 + if:
首先左边界start和右边界end在0处,若计算出sum >= target
就【记录最小子数组长度】再【右移start】,否则右移end,移完就再计算再移,直到end超出数组。
即外层循环的继续条件是end < nums.length
,内层if的条件是sum >= target
。
这两种思路都是每个元素进入视窗一次,退出视窗一次,时间复杂度没区别,都是O(2n)
,即O(n)
。感觉这题的边界、顺序还是挺容易搞错的,要多想想,多自测。