题目:
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。
示例:
输入:s = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
进阶:
如果你已经完成了 O(n) 时间复杂度的解法, 请尝试 O(n log n) 时间复杂度的解法。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/minimum-size-subarray-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
方法一:暴力法
暴力法是最直观的方法。初始化子数组的最小长度为无穷大,枚举数组 nums 中的每个下标作为子数组的开始下标,对于每个开始下标 i,需要找到大于或等于 i的最小下标 j,使得从 nums[i] 到 nums[j] 的元素和大于或等于 s,并更新子数组的最小长度(此时子数组的长度是 j−i+1)。复杂度分析:
时间复杂度:O(n^2),其中 n 是数组的长度。需要遍历每个下标作为子数组的开始下标,对于每个开始下标,需要遍历其后面的下标得到长度最小的子数组。
空间复杂度:O(1)
方法二:前缀 + 二分查找(未实现)
参考官方解析:
链接:https://leetcode-cn.com/problems/minimum-size-subarray-sum/solution/chang-du-zui-xiao-de-zi-shu-zu-by-leetcode-solutio/
方法三:双指针(滑动窗口)
在方法一和方法二中,都是每次确定子数组的开始下标,然后得到长度最小的子数组,因此时间复杂度较高。为了降低时间复杂度,可以使用双指针的方法。定义两个指针 start 和 end 分别表示子数组的开始位置和结束位置,维护变量 sum 存储子数组中的元素和(即从 nums[start] 到 nums[end] 的元素和)。
初始状态下,start 和 end 都指向下标 0,sum 的值为 0。
每一轮迭代,将 nums[end] 加到}sum,如果 sum≥s,则更新子数组的最小长度(此时子数组的长度是end−start+1),然后将 nums[start] 从 sum 中减去并将 start 右移,直到 sum<s,在此过程中同样更新子数组的最小长度。在每一轮迭代的最后,将 end 右移。
拓展:
算法框架逻辑:
int left = 0, right = 0; while (right < s.size()) { // 增大窗口 window.add(s[right]); right++; while (window needs shrink) { // 缩小窗口 window.remove(s[left]); left++; } }
以下来自:labuladong的算法小抄
/* 滑动窗口算法框架 */ void slidingWindow(string s, string t) { unordered_map<char, int> need, window; for (char c : t) need[c]++; int left = 0, right = 0; int valid = 0; while (right < s.size()) { // c 是将移入窗口的字符 char c = s[right]; // 右移窗口 right++; // 进行窗口内数据的一系列更新 ... /*** debug 输出的位置 ***/ printf("window: [%d, %d)\n", left, right); /********************/ // 判断左侧窗口是否要收缩 while (window needs shrink) { // d 是将移出窗口的字符 char d = s[left]; // 左移窗口 left++; // 进行窗口内数据的一系列更新 ... } } }
其中两处
...
表示的更新窗口数据的地方,到时候你直接往里面填就行了。而且,这两个...
处的操作分别是右移和左移窗口更新操作,它们操作是完全对称的。滑动窗口算法的思路是这样:
1、我们在字符串
S
中使用双指针中的左右指针技巧,初始化left = right = 0
,把索引左闭右开区间[left, right)
称为一个「窗口」。2、我们先不断地增加
right
指针扩大窗口[left, right)
,直到窗口中的字符串符合要求(包含了T
中的所有字符)。3、此时,我们停止增加
right
,转而不断增加left
指针缩小窗口[left, right)
,直到窗口中的字符串不再符合要求(不包含T
中的所有字符了)。同时,每次增加left
,我们都要更新一轮结果。4、重复第 2 和第 3 步,直到
right
到达字符串S
的尽头。这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
算法框架理解,
needs
和window
相当于计数器,分别记录T
中字符出现次数和「窗口」中的相应字符的出现次数。初始状态:
增加
right
,直到窗口[left, right)
包含了T
中所有字符:现在开始增加
left
,缩小窗口[left, right)
。直到窗口中的字符串不再符合要求,
left
不再继续移动。之后重复上述过程,先移动
right
,再移动left
…… 直到right
指针到达字符串S
的末端,算法结束。参考文献:
https://zhuanlan.zhihu.com/p/107786714
相似思路的另外一题目:
leet3. 无重复字符的最长子串 & C++ string长度 &滑动窗口&C++ STL unordered_set https://blog.csdn.net/Happy_Yu_Life/article/details/110368068
源码:
//F1:暴力法
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
//数组长度
int len = nums.size();
//判断特殊情况
if (!len){
return 0;
}
//初始长度为最大值
int res = INT_MAX;
//存放当前和
int sum =0;
//两个工作的节点i,j
int i,j=0;
//外层循环控制起始于元素
for( i=0;i<len;++i){
sum = 0;
//内层循环控制终止元素,即子数组
for(j=i;j<len;++j){
sum += nums[j];
//如果添加nums[j]后满足要求
if (s<=sum){
res = min(res,j-i+1);
break;
}
}
}
return res!=INT_MAX?res:0;
}
};
class Solution(object):
def minSubArrayLen(self, s, nums):
"""
:type s: int
:type nums: List[int]
:rtype: int
"""
###判断非空
if not nums:
return 0
##获取长度
n = len(nums)
sum_num ,start , end = 0,0,0
####设置初始res
res = n+1
###循环判定,控制最后一个loc
while end<n:
sum_num += nums[end]
##判断是否满足要求 s <= sum_num
while sum_num >= s:
res = min(res,end-start+1)
#判定是否满足条件
sum_num -= nums[start]
##头指针后移
start +=1
end +=1
return 0 if res == n+1 else res