算法力扣刷题记录四【长度最小的子数组】

前言

第四篇,继续。
记录四:力扣【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)) 时间复杂度的解法。

题目理解

  1. 给定数组:元素都是正整数,没有排顺序,没有0和负数;target也是正整数。
  2. 找到的子数组是输入的一个子集。并且“最短”。
  3. 3个示例,展示结果,理解功能。那怎么实现呢?

第一次尝试

思路过程

  1. 先不考虑时间复杂度,能实现再说。经过前两篇删除和平方发现,操作数组用双指针很有用。所以,我先考虑双指针行不行:从两端往中间靠拢或者从中间往两端分开,进入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。
    
  2. 怎么确定子数组是最短
    回到题目分析:

    • 整个数组中有一个元素 > 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也求不出来最短子集。
  1. 继续想怎么确定最短?

回到题目分析:

  • 枚举行不行?
    • 完整求和 < 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返回结果,所以找不出整个范围的“最短”长度。

(欢迎指正,转载标明出处)

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值