代码随想录算法训练营 |长度最小的子数组

代码随想录

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)。

视窗法

实际编写视窗法的三个易错点:

  1. 何时移动起始位置?
  2. 何时移动终止位置?
  3. 终止位置有需要左移的情况吗?

看了一下视窗法的基本原理后自己写了一下:

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;
};

我第一次自己写时犯过如下错误:

  1. 我是每次移动视窗边缘后,重头计算一次视窗里所有数的总和,导致时间复杂度直接提升到O(n^2)了。其实视窗每次只移动一位,可以每次在原来的总和基础上加上一个进入视窗的数,或减少一个退出视窗的数。
  2. 我设置了最小子数组长度minLen的初始值为0,导致没有新长度比初始值小。我每次找到新长度就拿来和minLen比较,把更小的话赋值给minLength,但是根本没有新长度比0小。
    • 方案1:minLen初始值不要设为数字,最后返回时判断一下minLen有没有变成数字,比如JavaScript语言中的Infinity属性,表示无穷数分为正无穷(Infinity)和负无穷(-Infinity)。
    • 方案2:两数比较时,对minLen===0的情况特殊处理一下。
  3. 每次找到视窗内元素之和大于等于target时,下一步左边缘肯定要往右移一位,元素之和才会下降,那右边缘要不要动呢?
    • 我本来想的是右边缘应该往左收缩,缩到左边源旁边,以免遗漏一些左边源移动后的视窗。
    • 但是看解析里写的是左边缘右移一位,右边缘不动。想了一下是应该如此,会被遗漏的那些情况本身很明显是元素之和小于target的,没必要考虑。
  4. 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循化。
  5. 一开始视窗内元素和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)。感觉这题的边界、顺序还是挺容易搞错的,要多想想,多自测。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值