和大于等于target的最短子数组 | 循序递进---@二十一画

题目:

给定一个含有 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)) 时间复杂度的解法。

分析:

拆解关键字:

【正整数、大于等于目标值、连续数组、最小连续数组、返回最小数组长度、不存在则返回0】

想法:
  • 小白解法:暴力破解

    –使用双重循环,从每一个数组的位置向后寻找满足的数组,这里我描述完几种解法在下面文字中会着重解释下,**【为什么要从每一个数组的位置向后寻找,而不是向前向后一起扩大范围】**这点才是解答这道题的关键,如果不明白这个,那么什么思路都不可能想出来,或者就算解答出来,你也不知道为什么这么写是对的🐶

  • 前缀法+二分查找

  • 滑动窗口

解释:

假设给定一个数组**【2,3,1,2】**,该数组存在的全部连续子数组的情况有如下:数组长度最小是1,最大是本身的长度。

⚠️分数组长度:

  • 数组长度为1: 【2】【3】【1】【2】

  • 数组长度为2: 【2,3】【3,1】【1,2】

  • 数组长度为3:【2,3,1】【3,1,2】

  • 数组长度为4:【2,3,1,2】

上面这种很好理解吧,就是分大小然后单独列出来,我这里将上面的数据换种方式展示【注意是一模一样的数据,只是换了分类方式】

⚠️分数组起始位置:index是下标位置 这里index的范围是03

  • index=0: 【2】【2,3】【2,3,1】【2,3,1,2】
  • index=1: 【3】【3,1】【3,1,2】
  • iddex=2: 【1】【1,2】
  • index=3: 【2】

可以发现,从起始位置开始向后扩范围,可以将所有的可能解挨个循环,这样可以保证不漏掉任何一个连续子数组。

所以我在描述暴力破解的时候,只需要每次从一个下标循环,然后在这个下标位置下二次循环,向后每次扩大一位数组,是可以得出最优解的。

代码:

第一版:暴力破解

image.png

class Solution {
    public int minSubArrayLen(int target, int[] nums) {

        return first0(target,nums);

    }


    public static int first0(int target,int[] nums){

        /*
        min_len作为最后的解,在循环过程中不断变化
        这里赋值 nums_len+1  只是为了比正式解要大一点,不能影响正确结果,不可以设置一个太小的值
        */
        int nums_len = nums.length;
        int min_len = nums_len+1;

        //开始双重循环。不断向后扩大范围。直到满足大于等于 target
        for(int i=0;i<nums_len;i++){

            int sum = nums[i];
            //如果找到一个元素 本身大于目标值,那么直接返回1 1就是最小的长度
            if(sum>=target){
                return 1;
            }

            // 向后每次扩大一位
            for(int j=i+1;j<nums_len;j++){

                sum += nums[j]; 
                //如果大于,那么和当前的最小数组范围对比,将较小的更新
                if(sum>=target){
                    min_len = Math.min(min_len,j-i+1);
                }
                
            }
        }

        // 如果循环后发现min_len 没有变化过,那么说明没有找到解,返回0即可
        return (min_len==(nums_len+1))?0:min_len;

    }


}
第二版:前缀和 + 二分查找

实现思路大致上已经有了方向,现在的目标是如何将这个过程不断优化,暴力破解是两次嵌套循环,来寻找当前元素往后一位一位扩大的sum

现在有一种方式,可以遍历一次就知道 暴力解法中数组sum的每一项值。

设一个数组还是sum吧,sum[i] 就等于从index=0index=i的全部元素之和。

那么遍历一次,肯定是可以填充完毕sum数组的。

那么 如果需要第1位到第4位的和怎么获取,那就是sum[4]-sum[0]sum[4]nums[0]nums[1],nums[2],nums[3],nums[4]的和,sum[0]nums[0]的和,所以二者相减就是第一位到第四位的和】

以上所述的就是前缀法

再看:【2,3,1,2】当一次遍历后,sum数组应该如下:【假设target=6】

i0123
sum[i]2568

如果以i=0开始,很好判断nums[2]=6,满足条件,结果占用的数组位数是 2-0+1 = 3位

如果从i=1开始呢,那么此时sum[i]就不是5了,这里是沾了前一个元素的光,所以才会累加成5。所以,因为此时sum[1]比本身扩大了sum[1-0]也就是2,所以对应的就不能寻找一个大于等于target的值就完事了,他作弊了,所以也要对应增加难度,就要寻找大于等于target+2的值【这个应该可以理解吧🐶】,所以从i=1开始,需要寻找大于8【6+2】的位置,那就是sum[3]。结果占用的数组位数是 3-1+1 = 3位

以下类推都是如此:

上面衍生出一个问题,是如何确认当前哪一个位置大于target了?就按照i=0来看吧,因为数据都是正数,累加肯定是芝麻开花节节高,一个比一个大,所以是升序数组。问题来了:

在一个升序数组中,寻找第一个大于等于target的下标位置?

🤔这是二分查找的应用场景吧

所以解决方式就都想清楚了:

  • 遍历一次,填充好sum数组
  • 遍历第二次,以当前遍历位置开始,寻找第一个大于等于target的位置
  • 更新结果
  • 返回

image.png

class Solution {
    public int minSubArrayLen(int target, int[] nums) {

        return first02(target,nums);

    }

    // 前缀法 + 二分查找
    public static int first02(int target,int[] nums){
        
        int nums_len = nums.length;
        int min_len = nums_len+1;
        int[] sum = new int[nums_len+1]; //为了保证sum[i]是前i个元素,否则sum[1]表示了前2个元素,这样下面容易混乱
        
        //遍历一次,填充好sum数组
        for(int i=1;i<=nums_len;i++){

            sum[i] = sum[i-1] + nums[i-1];
        }

        for(int i=1;i<nums_len+1;i++){
            //给当前寻找target增加难度 target的值要加上当前i位的sum[i]扩充的值
            //本身这个应该是num[i],但是此时sum[i] = num[i]+nums[i-1]+..nums[0],  具体解析看上面文字,
            int new_target = target + sum[i-1];
            sum[i-1]=0;//防止干扰后续二分查找 前面的sum值清0
            //自定义二分查找,恒定返回大于等于target的第一个下标
            int pos = myBinarySearch(sum,new_target); 
						
            //如果pos返回大于等于数组长度,那么证明在数组中不存在大于等于target。就不做处理
            //如果不大于,那么执行下面的逻辑,更新结果min_len 
            if(pos<nums_len+1){
                min_len = Math.min(min_len,pos-i+1);
            }

        }
        // 如果循环后发现min_len 没有变化过,那么说明没有找到解,返回0即可
        return min_len==nums_len+1?0:min_len;
    }

    //寻找第一个大于target的下标位置
    public static int myBinarySearch(int[] arr,int target){

        int left = 0;
        int right = arr.length-1;

        while(left<=right){

            int mid = (left+right)/2;
            if(arr[mid]==target){
                return mid;
            }else if(arr[mid]<target){
                left = mid + 1;
            }else{
                right = mid -1;
            }
        }
        return left;
    }


}
第三版:滑动窗口

看完上面两个解释,滑动窗口就比较容易理解了。滑动窗口由两个指针组成,left right,开始位置都是指向index=0。

  • left不移动,right不断右移寻找满足累加left和right之前的数和为sum,寻找sum大于等于target的解,直到找到解
  • 找到解之后,right不动,left开始不断右移,同时sum减去left移动的元素,这个过程是寻找潜在的最优解。在left移动中如果还满足sum大于等于target,那么这就是一个潜在的优于上一个的解。
  • 在left移动中,如果发现sum不满足大于等于target,会再次开始移动right。
  • 如此循环往复。。直到left和right都移动到数组末尾

以上就是滑动窗口的实现过程。之所以在寻找到一个解时继续滑动left,其实上面解释过了,如果left一直不动,right移动的话,这个解只会越来越冗余。

另外很显然,下面index假设为指针left,left不变的情况下,寻找到的第一个解就是当前以index为开始位置的最优解,越往后数组元素越多,肯定不是题目要求的最小连续。

这个时候,必须求变,那就是修改index的位置,在滑动窗口中体现出来就是left开始右移。

  • index=0: 【2】【2,3】【2,3,1】【2,3,1,2】
  • index=1: 【3】【3,1】【3,1,2】
  • iddex=2: 【1】【1,2】
  • index=3: 【2】

image.png

// 滑动窗口
    public static int first03(int target,int[] nums){

        int left = 0;
        int right = 0;
        int sum = 0;
        int nums_len = nums.length;
        int min_len = nums.length+1;

        while(right<nums_len){

            sum +=nums[right];

            while(sum>=target){
                min_len = Math.min(min_len,right-left+1);
                sum-=nums[left++];
            }
            //这里++要置后,否则right-left+1 上面的结果就不正确了
            right++;
        }
        
        return min_len==nums_len+1?0:min_len;
    }

总结:

这道题前期要清楚从每个index开始向后扩大的逻辑。

后期滑动窗口说起来简单,但是实现起来也需要一些清晰的逻辑,否则就是原地转圈😓

这道题主要是介绍前缀和还有滑动窗口💪🏻

大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值