题目
剑指 Offer II 008. 和大于等于 target 的最短子数组
思路
本题使用双指针的思路
一个子数组可以用两个指针表示:
left
:指向子数组最左边的下标
right
:指向子数组最右边的下标
暴力法
这道题的本质还是检查所有子数组,找出其和大于等于target,且长度最小的那个
如果单纯地遍历所有子数组,再对每个子数组求和,其时间复杂度为O(n ^ 3)
当然可以进行优化,比如预先处理出前缀和数组,这样对每个子数组求和这步操作就从O(n)
降为O(1)
,总体时间复杂度降为O(n ^ 2)
双指针
我们来看看双指针解法:
一开始left = 0,right = 0
,其代表的子数组中只有一个元素,sum = nums[0]
然后检查sum和target的关系:
sum < target
:说明以left开头的子数组,如果以right结尾,其和不满足条件,下一步要扩大right,看扩大后是否能满足条件,并同步更新sum
-
sum >= target
:说明以left开头的子数组,如果以right记为,是满足条件的:- 如果right再往右,也满足条件,但就不是长度最小的那个子数组了,因为其长度都大于
[left,right]
这个子数组,因此后面的子数组没有必要再去检查 - 如果right往左,就不满足条件,可以排除(之前已经排除过了)
- 因此我们等于是检查了所有以left为开始的子数组
- 如果right再往右,也满足条件,但就不是长度最小的那个子数组了,因为其长度都大于
-
下一步:需要将left++。从sum减去nums[left],继续检查sum 和 target的关系,也就是检查下一个left
- 这里为什么right不用重置为left,从[left,left]开始检查以left开始的每个子数组?
- 答案是不用检查
- 因为上一次循环从left-1开始(上一次循环的left),直到遇到right,sum才大于等于target,也就是说:从left-1一直到right-1,sum都小于target
- 而本次循环left比上一次大1,从left到right-1的元素更少了,其和只可能更少,因此一样是小于target。而小于target的部分我们可以忽略掉,因为不满足条件,直接从right开始
- 因此right指针不用回退!
示例
以target = 7, nums = [5,1,4,3]为例:
- left,right一开始都指向5,sum = 5,小于7,right++,变为1
- 此时数组中包含两个数字5,1。sum = 6,任然小于7,right++,变为2
- 此时数组包含3个数字5,1,4。sum = 10,大于7,因此记录答案,长度为3
- 左移left,此时sum = 10 - 5 = 5,小于7,停止左移left,继续右移right
- 这样不断移动right,left,直到right大于nums.length为止
正确性证明
可以看出整个过程检查了以每个left为左端点的所有子数组,不会漏过任何一个子数组,因此计算出来的结果也是正确的
时间复杂度
由于left,right不会回退,最多从0到nums.length,因此时间复杂度为O(n)
可以看出,双指针法用O(N)的时间复杂度,就检查完了所有的子数组,就是因为其跳过了很多不可能是答案的子数组
代码
public int minSubArrayLen(int target, int[] nums) {
int left = 0;
int right = 0;
int sum = 0;
int minLen = Integer.MAX_VALUE;
while (right < nums.length) {
// 将nums[right]累加到sum
sum += nums[right];
if (sum < target) {
right++;
continue;
}
// 如果满足条件
while (sum >= target) {
// 计算出长度最小的那个
minLen = Math.min(minLen,right - left + 1);
sum -= nums[left];
left++;
}
right++;
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}