给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。示例 2:
输入:target = 4, nums = [1,4,4]
输出:1示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0提示:
1 <= target <= 1091 <= nums.length <= 1051 <= nums[i] <= 105
暴力破解(超时)
首先我们不去管其它解法,就单单暴力解法来说,我们也得先了解它是怎么走的!
暴力解法的思路比较简单,就是枚举所有的子数组,看看那些数组和满足要求的数组中长度最小的那个,那么无非就是套两层循环,用 left 表示数组的起点,用 right 表示数组的结尾,然后用变量 sum 记录下子数组的和,这样子就不用再套一层循环去求和,还要用一个变量 minlen 记录下每次符合要求的最小长度!
并且我们可以做点小优化,在每次遍历 right 的时候,如果遇到了满足要求的时候,其实此时对于以 left 开头的子数组来说就是最短长度的了,所以就可以直接 break,遍历其它以 left 开头的子数组了!如下图所示:

class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int minlen = INT_MAX;
for(int left = 0; left < nums.size(); ++left)
{
int sum = 0; // 记录子数组的和
for(int right = left; right < nums.size(); ++right)
{
sum += nums[right]; // 累加当前位置的值
if(sum >= target)
{
// 更新结果,因为left开头的最短区间已经找到,所以直接break
minlen = min(minlen, right - left + 1);
break;
}
}
}
return minlen == INT_MAX ? 0 : minlen;
}
}; 但是这种解法对于中等题来说肯定就超时啦,所以我们必须再做优化!
💥滑动窗口
暴力破解的思想是没问题的,但是缺点就在于每次 right 走完之后都需要回退,这就导致 O(n^2) 的时间复杂度,所以我们要想办法看看能不能让 left 和 right 都是不回退,一直向后走的!
答案肯定是可以的,其实这道题有一个条件很重要,就是数组中元素的值都是正的!这有什么用呢❓❓❓
仔细想想,要是所有元素的值都是正的,那么在子数组中就不存在说遍历一个元素之后总和会变小的情况,也就是说,每次 right++ 之后,总和都会变大,这点非常关键!
再仔细想想,如果说数组是不断变大的,那么我们每次让 right 回退到 left 处开始,岂不是之前计算的总和就白干了,对不对!所以说我们要利用好之前算的那个满足要求的总和!
那么怎么利用呢❓❓❓
此时一种算法思想:“滑动窗口”,就由此诞生了,不要觉得听起来很高级,其实就是一种双指针的遍历方式,看起来很像一个窗口在动罢了!
切记,我们不要想着如何使用 “滑动窗口” 来解决问题,而是要思考,为什么要用 “滑动窗口”❓❓❓
因为 “滑动窗口” 的核心就是控制窗口的左右边界进行出入窗口,其达到的效果就是让两个指针只需要遍历一次就能处理完问题!但是使用 “滑动窗口” 是有限制条件!
对于这道题来说,只要出现了满足要求的组合之后,根据单调性,后续的长度就会增大,那么就没有遍历意义了!
算法流程
-
定义双指针,
left表示窗口左边界,right表示窗口右边界 -
遍历
right,用变量sum累加上每个nums[right]的值,相当于让nums[right]进窗口!-
此时如果
sum >= target,说明该子数组满足要求,那么用一个变量minlen记录下每次的满足要求的子数组的最短长度! -
并且我们要做一个操作,就是先让
sum减去nums[left],然后让left++,相当于让 nums[left] 元素出窗口!- 为什么可以这么做呢❓❓❓
- 还记得我们讲暴力破解的时候,如果以
left开头找到了满足要求的子数组,此时该子数组的长度就是最短的,所以对于left开头的子数组来说就没必要让right继续往后走了!直接让left++,但是这里不同的是,我们不用让right回退,就保留了之前所求的和,我们就不用让right回退重新计算!
-
-
重复上述操作,直到
right走出数组边界!
💥纠错:上面第一步中的 minlen 应该是为 INT_MAX 才对!
这样子通过双指针的移动,看起来就像是一个窗口在平移,所以才叫做 “滑动窗口”!
而这个窗口,其实就是由 left、right 以及 sum 的值来维护,对 sum 累加相当于进窗口,对 sum 减值相当于出窗口!
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0; // 子数组起始指针
int minlen = INT_MAX;
int sum = 0;
for(int right = 0; right < nums.size(); ++right)
{
sum += nums[right]; // 进窗口
while(sum >= target)
{
minlen = min(minlen, right - left + 1); // 先计算最小长度
sum -= nums[left]; // 出窗口
left++;
}
}
return (minlen == INT_MAX ? 0 : minlen);
}
};