题目:
给定一个正整数数组 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
,代表当前是一个解,如此遍历,直到i
和j
遍历结束,可以返回解的个数 - 这里可以做优化,就是
j
一直累加,sum
一直累乘,直到sum
大于等于k
,此时可以直接得出i=>j-1
之间的元素乘积都是小于k
的,可以直接求出以i
为起始位置的解的个数 - 在
i
和j
的循环中,如果当前i
所维护的sum
值大于等于目标值k
,那么代表后续j
继续增加直到导致sum
越来越大,肯定也是比k
大了,就不需要在继续当前i
的循环了,i++
继续下次外循环即可 - 由此便可以循环全部存在的子数组,确定最终存在几个解
- 设
-
滑动窗口
💡
- 定义两个指针
left
,right
,初始位置都是0,定义time
值,作为从left
–right
的全部元素的累乘值 - 当
time
小于k
,time*=nums[right]
- 当
time
大于等于k
,记录此时right
的位置,得出,right-1
的位置 当前time
是小于k
的,代表了从当前的left-->right-1
的子数组中,从left
为起点,一直到right-1
的全部子数组乘积都小于k
【上面解释过了】,需要更新解 time
大于等于k
的时候,更新解之后,将left++
,不断的寻找其他解- 直到
left=right
- 定义两个指针
代码:
第一版:暴力破解
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;
}
}
第二版:滑动窗口
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;
}
}
第三版:滑动窗口优化代码
这版滑动窗口是我观看别人时间借鉴的。我的滑动窗口是每次寻找到最大的left
和right
边界,然后一次性求出以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
就可以当不满足
left
到right
小于k
的时候,再移动left
,进行寻找下一个滑动窗口的解。⚠️⚠️⚠️⚠️:这里的重要是每次累加的是新的以
right
为结束位置的子数组的解!!!⚠️⚠️⚠️⚠️
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
条件,可能有时候多限制一个就错误,少限制一个就异常,这道题确实不太容易,需要时刻回顾,笔者第一天写完运行通过,第二天再写发现思路又错了,说明这道题明白思路还不行,很多细节需要不断的完善,我也是不断在idea
上debug
才最后完成的。
哪怕解决的思路是一样的,但是每个人写出来具体细节是千差万别的,很多东西需要不断沉淀,才能真正变成一种属于自己的解题办法。
大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊