前言
第四篇,继续。
记录四:力扣【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
解释:子数组[4]满足
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
解释:全数组之和小于11,没有满足的子数组。返回0.
提示:
1 <= target <= 109
1 <= nums.length <= 105
1 <= nums[i] <= 105
进阶:
如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
题目理解
- 给定数组:元素都是正整数,没有排顺序,没有0和负数;target也是正整数。
- 找到的子数组是输入的一个子集。并且“最短”。
- 3个示例,展示结果,理解功能。那怎么实现呢?
第一次尝试
思路过程
-
先不考虑时间复杂度,能实现再说。经过前两篇删除和平方发现,操作数组用双指针很有用。所以,我先考虑双指针行不行:从两端往中间靠拢或者从中间往两端分开,进入for循环后if判断:
int sum = 0; //记录子数组之和 int num = 0; //返回值,子数组的长度 if(nums[i] >target || nums[j] >target){ return 1; //子数组长度是1 }else if(nums[i] < nums[j]) { num++; sum += nums[j]; if(sum > target) //如果求和满足条件,返回num return num; j--; }else ……
问题:上面的部分逻辑错误在于,我没有找到最短的子数组。
当双指针从两端开始时: 看谁先达到sum > target。 当双指针从中间开始时,同理,看向左/右,哪个先实现 累计sum > target。
-
怎么确定子数组是最短?
回到题目分析:- 整个数组中有一个元素 > target,那么返回值=1;
- 整个数组所有元素都 < target,此时数组中最大的那个元素肯定得在最短子集中。所以需要一个base,作为子集的基准,从base向两边扩展,依靠两个指针i,j,求得sum > target。
- base是当前循环到的最大元素,当寻到一个元素A > base,base就重新定位到A。那么base从0开始向右移动?还是把base初始定在数组的正中间?(我先选择正中间,感觉而已,可能更快。)
好像行得通。尝试去写。
没有写出来,过程中遇到的逻辑错误:
1 base指向最大元素的循环:错误经历很多。下面是其中一步:
for(int i = base-1,j = base+1;(i >=0)||(j <=nums.size());){
if(nums[base] <target){
if((nums[i] < nums[base])&&(nums[j] < nums[base])){
if(nums[i] < nums[j]){ //暂时没考虑等号
sum += nums[j];
num++;
j++;
}else{
sum += nums[i];
num++;
i--;
}
if(sum > target){ //错误,没有遍历完,还不是最短的子数组。
break;
}
}else{
nums[base] = (nums[i]>nums[j])?nums[i]:nums[j]; //替换为nums[i]和nums[j]中max
num = 1; //重新计数,一切更新。
i = base-1;
j = base+1;
sum = nums[base];
}
}else{ //数组中某个元素>target,所以就他自己,return 1.
num++;
break;
}
}
2 最后发现:“数组中最大的那个元素肯定得在最短子集中”,这句话错误。
比如:[2,3,5,4,1,7,1] target=9:其中[5,4]最短,但没有元素7。
所以base指向max也求不出来最短子集。
- 继续想怎么确定最短?
回到题目分析:
- 枚举行不行?
- 完整求和 < target,return 0;
- 有一个元素 > target,return 1;
- 剩余子集很多,不行。关键就这里怎么处理?
放弃,没写出来。
代码随想录学习
学习内容
(1)暴力解法:两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是O(n^2)。
如何实现?(好吧,枚举的思想没写出来。)
思路:外层for循环是子集的开头,内层for循环是子集的结束。
没写出来的原因:考虑枚举的时候,从子数组长度1,2,3,……角度去枚举,很杂乱,所以感觉子集数量众多,枚举不尽,没有头绪。
但两个for循环:内循环——确定了一端,找到该起始元素下符合条件的最短子数组。外循环——依次换起始元素,result得到整个数组的最短子集。
//知道思路之后,自己写一遍。(有错)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int sublength = 0;
int result = INT32_MAX; //系统定义的宏,代表32位系统中的最大值。
int sum = 0;
for(int i = 0;i < nums.size();i++ ){
sum = 0; //每一次换个起始,都得重新求和
sublength = 0;
for(int j = i; j < nums.size(); j++){
sum += nums[j]; //起始元素也要算上,所以初始j=i
sublength++;
if(sum > target){ //只要符合条件就换一个起始,但此时只是中间结果,不是最短区间
result = (result < sublength)? result : sublength; //确定整个数组内的最短子数组
break; //在该起始元素下,满足条件的最短子数组
}
}
}
//当没有符合的子数组时,result还是初始值。
result = (result < INT32_MAX) ? result : 0;
return result;
};
对比区别:
1 sublength的处理:
我:用了两行代码,每次换个起始端,sublength=0;内循环中sublength++;
参考:只用一行:在if(sum > target)下 改成sublength = j -i +1;因为此时已经确定区间的另一端。
2 return 处理,result如果没有赋值,返回0:
我:用了两行。还比较一遍(result < INT32_MAX) ? result : 0;
参考:直接return result == INT32_MAX ? 0 : result;
两处地方更简洁,代码更少。
3 另外判题之后,逻辑错误,输出结果比正确的多1,本应该输出2,结果输出3:if(sum > target)少了“=”。
所以:正确解法一改进,下面的没错误:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int sublength = 0;
int result = INT32_MAX; //系统定义的宏,代表32位系统中的最大值。
int sum = 0;
for(int i = 0;i < nums.size();i++ ){
sum = 0; //每一次换个起始,都得重新求和
//sublength = 0;不要了
for(int j = i; j < nums.size(); j++){
sum += nums[j]; //起始元素也要算上,所以初始j=i
//sublength++;不要了
if(sum >= target){ //只要符合条件就换一个起始,但此时只是中间结果,不是最短区间
sublength = j -i+1; //新加
result = (result < sublength)? result : sublength; //确定整个数组内的最短子数组
break; //在该起始元素下,满足条件的最短子数组
}
//sublength++;
}
}
//当没有符合的子数组时,result还是初始值。
//result = (result < INT32_MAX) ? result : 0; 下一行替换。
return result == INT32_MAX ? 0 : result;
}
};
(2)滑动窗口解法:
如何实现?用一个for循环完成。
思路:
(1)首先滑动窗口,有一个起始和一个终点。外循环for(j = 0; j < nums.size();j++),j代表滑动窗口的末端。
(2)窗口的末尾从头向后滑动,窗口逐渐展开,等到sum >= target后,窗口的起始开始移动,收缩窗口,直到窗口内sum >= target的最小值。“直到”这两个字体现:while循环。
(3) while结束,只是当前窗口末尾的结束,而不是整个数组的结束。所以j需要走到nums.size(),坚持遍历整个数组。
(4)最短长度result记录:整个过程只有比当前result更小才会替换。
//滑动窗口实现
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int sublength = 0;
int result = INT32_MAX; //系统定义的宏,代表32位系统中的最大值。
int sum = 0;
for(int i = 0,j = 0;j < nums.size();j++){ //滑动窗口的末尾在后移
sum += nums[j];
while(sum >= target){ //滑动窗口收缩。
sublength = j-i+1;
result = (result < sublength) ? result : sublength;//选择更小的
sum -=nums[i]; //先减
i++; //后操作i
}
}
//当没有符合的子数组时,result还是初始值。
return result == INT32_MAX ? 0 : result;
}
`注意点:
1 while出来还需要继续移动j,所以没有循环跳出语句,整个数组没有结束。
2 while内先sum做减法,再i++;
3 sublength不能再每次重复计数,因为“滑动窗口”是在上一个的基础上往后移,所以不能sublength清零后再++。
4 j代表窗口的末尾,如果代表窗口起始,那么和解法一没有区别。
5 时间复杂度分析:窗口移动的过程,每个元素通过j纳入窗口,再通过i流出窗口(要么就不流出),最坏情况操作一遍完整数组。所以复杂度是O(n)。
总结
反思
为什么没写出来,看完思路才做出?
答:一上来通过实例看到结果,发现子数组是在中间才找到,没有一个统一的顺序。从子数组长度角度出发,发现子数组众多,感觉枚举不尽,所以思路没有。
收获
- “滑动窗口”也是操作两个指针,所以双指针法得到第3个用法示例。
- 白话说“滑动窗口”——窗口逐步拉开,满足标准后,开始收缩窗口;不能再收缩时,继续后拉窗口扩大,再满足标准后,又开始收缩窗口。直到窗口拉倒整个数组的最后。
- 学习记录“最短”长度:因为没想到用result记录寻找过程中的最小值,让result始终保持当前“最短”。所以在遇到sum > target总想break返回结果,所以找不出整个范围的“最短”长度。
(欢迎指正,转载标明出处)