209.长度最小的子数组
给定一个含有 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 <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
进阶:
- 如果你已经实现
O(n)
时间复杂度的解法, 请尝试设计一个O(n log(n))
时间复杂度的解法。
思路:
要寻找符合条件的最小子数组,自然而然就可以想到通过把所有的子数组遍历一遍,找到符合要求的,再比较其中的长度最小的子数组。这样的话,要使用两层for循环,属于暴力解法,时间复杂度较高。
做这道题我们可以使用滑动窗口的方法,其实和双指针法是比较类似的。只不过移动的是整个一个区间,所以称为滑动窗口。
暴力解法:
class Solution {
public:
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; // 让result等于一个极大的值,INT32_MAX为宏定义
int sum = 0; // 存储子序列的数值之和
int subLength = 0; // 存储子序列的长度
for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
sum = 0;
for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
sum += nums[j];
if (sum >= s) { // 一旦发现子序列和超过了s,更新result
subLength = j - i + 1; // 取子序列的长度
result = result < subLength ? result : subLength; //比较更新result
break; //找到符合条件最短的子序列,退出循环
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
}
};
时间复杂度:O(n^2)
滑动窗口法:
我们可以使用滑动窗口法对时间复杂度进行优化,不断调节子序列的起始和终止位置。
设定两个指针i和j,分别指向滑动窗口的起始位置以及终止位置。
这里我们就需要考虑滑动窗口外层for循环中遍历的应该是窗口的起始位置还是终止位置。
如果起始位置一直在移动的话,不可避免的,我们在起始位置每次变化前,终止位置都需要对整条数组进行一遍遍历,若是这么做,那就与之前的暴力解法无异了。
所以我们采用循环的索引表示滑动窗口终止位置的方法,让j先去不断移动,用sum记录此时窗口中的和,一旦sum的值大于等于目标值,我们就可以对起始位置进行更新,缩小起始位置,从而判断是否存在更小长度,符合条件的子数组。
class Solution {
public:
//滑动窗口
int minSubArrayLen(int s, vector<int>& nums) {
int result = INT32_MAX; //让result取一个极大值
int sum = 0; // 滑动窗口数值之和
int i = 0; // 滑动窗口起始位置
int subLength = 0; // 滑动窗口的长度
for (int j = 0; j < nums.size(); j++) { //j指向滑动窗口的末尾位置,用来遍历
sum += nums[j];
// 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件
while (sum >= s) {
subLength = (j - i + 1); // 取子序列的长度
result = result < subLength ? result : subLength;
sum -= nums[i++]; // 不断变更i(子序列的起始位置)
}
}
// 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
return result == INT32_MAX ? 0 : result;
}
};
时间复杂度:O(2n) 可以视作 O(n)
二分查找法:
这种方法利用了之前所学到的二分查找法,先创建一个数组sums用于存储数组nums的前缀和。sums[i]表示nums[0]到nums[i-1]的元素和。
得到前缀和后,对于每个开始下标i,通过二分查找得到大于等于i的最小下标bound,使得sums[bound]-sums[i-1]大于等于s。并实时更新子数组的最小长度bound-(i-1)
这里使用二分查找的前提是前缀和数组sums是有序的,由于原数组中都是正整数,所以所得到的的前缀和数组也必定是递增的,所以我们可以使用二分查找的方法。
class Solution {
public:
//二分查找
int minSubArrayLen3(int s, vector<int>& nums) {
int n = nums.size();
if (n == 0) { //判断数组为空的情况
return 0;
}
int ans = INT_MAX; //先将结果值设置为一个极大值
vector<int> sums(n + 1, 0);
// 为了方便计算,令 size = n + 1
// sums[0] = 0 意味着前 0 个元素的前缀和为 0
// sums[1] = A[0] 前 1 个元素的前缀和为 A[0]
// 以此类推
for (int i = 1; i <= n; i++) {
sums[i] = sums[i - 1] + nums[i - 1];
}
for (int i = 1; i <= n; i++) {
int target = s + sums[i - 1];
auto bound = lower_bound(sums.begin(), sums.end(), target); //使用现成的库函数来实现这里二分查找大于等于某个数的第一个位置的功能
if (bound != sums.end()) {
ans = min(ans, static_cast<int>((bound - sums.begin()) - (i - 1)));
}
}
return ans == INT_MAX ? 0 : ans;
}
};
时间复杂度:O(nlogn)
后续也会坚持更新我的LeetCode刷题笔记,欢迎大家关注我,一起学习。
如果这篇文章对你有帮助,或者你喜欢这篇题解,可以给我点个赞哦。
CSDN同步更新,欢迎关注我的博客:一粒蛋TT的博客_CSDN博客-LeetCode学习笔记,HTML+CSS+JS,数据结构领域博主
往期回顾:
LeetCode977.有序数组的平方
LeetCode844.比较含退格的字符串
LeetCode283.移动零
LeetCode27.移除元素
LeetCode26.删除有序数组中的重复项