题目:
给定一个含有 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
的范围是0
到3
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】
可以发现,从起始位置开始向后扩范围,可以将所有的可能解挨个循环,这样可以保证不漏掉任何一个连续子数组。
所以我在描述暴力破解的时候,只需要每次从一个下标循环,然后在这个下标位置下二次循环,向后每次扩大一位数组,是可以得出最优解的。
代码:
第一版:暴力破解
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=0
到index=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】
i | 0 | 1 | 2 | 3 |
---|---|---|---|---|
sum[i] | 2 | 5 | 6 | 8 |
如果以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的位置
- 更新结果
- 返回
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】
// 滑动窗口
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开始向后扩大的逻辑。
后期滑动窗口说起来简单,但是实现起来也需要一些清晰的逻辑,否则就是原地转圈😓
这道题主要是介绍前缀和还有滑动窗口💪🏻
大家好,我是二十一画,感谢您的品读,如有帮助,不胜荣幸~😊