乘积小于K的子数组 | 循序递进---@二十一画

题目:

给定一个正整数数组 nums和整数 k ,请找出该数组内乘积小于 k 的连续的子数组的个数。

示例 1:

输入: nums = [10,5,2,6], k = 100
输出: 8
解释: 8 个乘积小于 100 的子数组分别为: [10], [5], [2], [6], [10,5], [5,2], [2,6], [5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于100的子数组。
示例 2:

输入: nums = [1,2,3], k = 0
输出: 0

提示:

1 <= nums.length <= 3 * 10^4
1 <= nums[i] <= 1000
0 <= k <= 10^6

分析:

拆解关键词:

【正整数数组、目标值整数K、连续子数组、乘积、小于、求连续子数组个数】

想法:
  • 暴力破解法
  • 滑动窗口

⚠️这里可能有人会使用前缀法,在实现的实现发现前缀法很容易出现数据溢出,导致结果不正确,代码维护其比较麻烦,这里建议使用滑动窗口,效率比前缀法要高。

解释:
  • 暴力破解法:

    💡

    • i是数组的下标,则i满足i∈[0,arr.length-1]. 定义两个循环,一内一外;
    • i的循环为外循环,for(int i=0;i<=arr.length-1;i++)j是内循环,j满足j∈[i+1,arr.length-1]while(j<arr.length-1)
    • 循环体中,存在一变量sum,该变量依存i的变化,也就是说该变量在每一个外循环中初始化sum=nums[i],在内循环中累乘sum*=nums[j]sum代表当前下标i到下标j的全部元素的乘积
    • 当乘积sum小于所给目标值k,代表当前是一个解,如此遍历,直到ij遍历结束,可以返回解的个数
    • 这里可以做优化,就是j一直累加,sum一直累乘,直到sum大于等于k,此时可以直接得出i=>j-1之间的元素乘积都是小于k的,可以直接求出以i为起始位置的解的个数
    • ij的循环中,如果当前i所维护的sum值大于等于目标值k,那么代表后续j继续增加直到导致sum越来越大,肯定也是比k大了,就不需要在继续当前i的循环了,i++继续下次外循环即可
    • 由此便可以循环全部存在的子数组,确定最终存在几个解
  • 滑动窗口

    💡

    • 定义两个指针leftright,初始位置都是0,定义time值,作为从leftright的全部元素的累乘值
    • time小于ktime*=nums[right]
    • time大于等于k,记录此时right的位置,得出,right-1的位置 当前time是小于k的,代表了从当前的left-->right-1的子数组中,从left为起点,一直到right-1的全部子数组乘积都小于k【上面解释过了】,需要更新解
    • time大于等于k的时候,更新解之后,将left++,不断的寻找其他解
    • 直到left=right

代码:

第一版:暴力破解

image.png

class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {

        return first01(nums,k);
    }

    public static int first01(int[] nums,int k){

        int res = 0;
        int len = nums.length;

        for(int i=0;i<len;i++){
            
            int sum = nums[i];
            if(sum>=k){
                //如果一个元素就大于等于k了,那么起点为i的其他元素就不用看了,累乘肯定大于等于k
                continue;
            }
            int j = i+1;
            while(j<len){
                sum *= nums[j];
                if(sum>=k){
                    break;
                }else{
                    j++;
                }
            }
            res += (j-i);
        }
        return res;

    }
}
第二版:滑动窗口

image.png

class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {

        if(k==0) return 0;
        return first02(nums,k);
    }

    public static int first02(int[] nums,int k){

        int left = 0;
        int right = 0;
        int time = 1;
        int len = nums.length;
        int res = 0;

        while(left < len && left<=right){

            //如果time小于k,那么time向后扩展一位
            if(time < k && right<len){
                time *= nums[right];
            }
            //如果time此时大于等于k了,那么代表此时left-->right-1的位置 乘积是小于k的 ,将这部分数据加入解,然后移动left继续寻求其他解
            while(time>=k && left <len && left <= right){

                res += (right-left);
                time/=nums[left];
                left++;
            }
            //定义退出条件  如果right已经移动到末端,此时time依旧小于k,那么代表当前left之后包括left在内到right之间全部的元素组合成的连续子数组都符合小于k的解,此时就不需循环了,退出更新全部的解即可
            if(right==len && time<k){
                for(int i=left;i<=len;i++){
                    //循环i,累加 分别以i为起始位置的子数组
                    res += (right-i);
                }
                return res;
            }
            //此时time小于k,right++,进行下一轮的循环
            right++;
        }
        return res;
    }
}
第三版:滑动窗口优化代码

这版滑动窗口是我观看别人时间借鉴的。我的滑动窗口是每次寻找到最大的leftright边界,然后一次性求出以left为起始点的满足乘积小于k的连续子数组的个数。这也就代表我每次计算的时候,我不能轻易移动left指针,而是必须要找到以当前left为不动点,right的最大边界。

这种写法导致我在代码上有很有限制。

下面的思路是:

  • 还是以left为不动点,每次right移动的时候都可以更新res,而不是等到right是最大极限的位置

  • 每次累加的是以right为结束位置,一共有几个满足乘积小于k的连续子数组

    举例:给定数据【1,2,3】,k = 100

    left=0,right=1,此时乘积nums[left]xnums[right]= 2,满足条件,以right结尾的数组【1,2】【2】所以累加;

    left=0,right=2,此时乘积nums[left] x ......x nums[right]= 6,满足条件,以right结尾的数组【1,2,3】【2,3】【3】所以再次累加3个;

    所以这种思路是可以一遍遍历一遍加,只要right大于等于left就可以

    当不满足leftright小于k的时候,再移动left,进行寻找下一个滑动窗口的解。

    ⚠️⚠️⚠️⚠️:这里的重要是每次累加的是新的以right为结束位置的子数组的解!!!⚠️⚠️⚠️⚠️

image.png

class Solution {
    public int numSubarrayProductLessThanK(int[] nums, int k) {

        return first03(nums,k);
    }

    /**
     * 循环开始:
     * 该方法每次移动right指针,去寻找以当前right为右边界的全部解!
         * ①如果当前left--right乘积大于等于k,那么此次循环不断left++,直到满足条件,此时left--right就是解
         * ②如果当前本身就满足乘积小于k,那么直接得到解。
     * right++
     * 循环结束:
     */
    public static int first03(int[] nums,int k){

        //先处理特殊值
        if(k<2)return 0;

        int left = 0;
        int right = 0;
        int time = 1;
        int res= 0;
        int len = nums.length;

        while(right<len){

            time *= nums[right];

            while(time>=k && left<=right){
                time/= nums[left];
                left++;
            }
            //[1 2 3 4 5] 可以推导出 假如left是3,right是5,以5结尾的数组 是 【3,4,5】【4,5】【5】
            // 5-3+1 就是当前位置的新出现的以right为结尾的子数组的解
            if(left<=right) res += (right-left+1);

            right++;

        }
        return res;
    }
}

总结:

说实话,本题在书写的时候思路特别绕,很多边界条件比如代码中的while条件,可能有时候多限制一个就错误,少限制一个就异常,这道题确实不太容易,需要时刻回顾,笔者第一天写完运行通过,第二天再写发现思路又错了,说明这道题明白思路还不行,很多细节需要不断的完善,我也是不断在ideadebug才最后完成的。

哪怕解决的思路是一样的,但是每个人写出来具体细节是千差万别的,很多东西需要不断沉淀,才能真正变成一种属于自己的解题办法。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值