长度最小的子数组
思路
暴力解法
- 看到这题,我们最容易想到的就是利用两层for循环求解。
- 外层for循环的i确定子数组的起始位置,内层循环的j确定子数组的终点位置
- 不断扩大子数组的长度(即j不断右移),直到子数组的和大于等于target
- 将起始位置右移,直到遍历完整个数组
- 不难得出这种暴力解法的时间复杂度为O(n 2),这种解法在leetcode上不能通过
滑动窗口
- 何为滑动窗口?就是不断调节子序列的起始位置和终点位置,从而得到我们想要的结果。
- 其实在暴力解法中,实际上也运用了滑动窗口的思想,只是运用了两层循环来实现窗口大小的改变。
- 那么我们如何用一个循环来实现滑动窗口,从而降低时间复杂度呢?
- 首先我们要考虑清楚这个循环改变的是窗口的终点位置还是起始位置。如果是改变起始位置i,那终止位置又怎么确定呢?是不是还是需要一层循环?这不就和暴力解法一样了吗?所以这个循环应该改变的是窗口的终点位置。
- 考虑好了循环的改变量,那我们接下来就考虑怎么改变滑动窗口(即怎么改变和什么时候改变滑动窗口的起始位置)
- 首先令终止位置j和起始位置i为整个数组的起点,代表开始滑动窗口的长度为1
- 接着不断右移终止位置(即扩大窗口),直到窗口内数据的和sum大于等于target,这时就要开始缩小窗口了
- 将起始位置不断左移,并将sum减去左移元素,同时记录窗口的大小(保留较小值),直到sum小于target
- 重复上述操作,直到j遍历完数组
- 可以分析出时间复杂度为O(n)
- 如图所示
实现代码
暴力解法
int minSubArrayLen(int target, int* nums, int numsSize){ int result = INT_MAX; //将result初始化为int类型的最大值 int sum = 0; //定义子数组的和 int sublength; //定义子数组长度 for(int i = 0; i < numsSize; i++) { sum = 0; for(int j = i; j < numsSize; j++) { sum += nums[j]; if(sum >= target) //只要子数组的和大于等于了target,那说明由初始位置确定的最短子数组已经出现 { sublength = j - i + 1; //求出这个子数组的长度 result = sublength < result ? sublength : result; //最终长度取较小值 break; } } } return result == INT_MAX ? 0 : result; //如果最终长度还是INT_MAX,说明result未变,说明无符合条件的子数组,返回0 }
滑动窗口
int minSubArrayLen(int target, int* nums, int numsSize){ int result = INT_MAX; int sublength; int sum = 0; int i = 0; for(int j = 0; j < numsSize; j++) { sum += nums[j]; //计算窗口内元素的值 while(sum >= target) { //缩短窗口长度,减小子数组大小 sum -= nums[i]; sublength = j - i + 1; i++; //改变起始位置 result = result < sublength ? result : sublength; //保留子数组长度的较小值 } } return result == INT_MAX ? 0 : result; //如果最终长度还是INT_MAX,说明result未变,说明无符合条件的子数组,返回0 }
Tips
- 滑动窗口这种方法只适用于数组元素都是正数的情况,只有这样才能确保扩大窗口(即终止位置右移)时sum只加不减,缩短窗口时(即起始位置左移)时sum只减不加的情况,防止产生错误。
- 有些同学可能会疑惑,每一次右移终止位置,为什么起始位置还是上一次的位置,而不要重新从原数组头开始。这是因为如果起始位置i从头开始,由于终止位置的右移(即滑动窗口扩大了一位),相较于终止位置未右移的滑动窗口,右移后窗口内元素的和一定大于等于未右移的窗口内元素和,因此i不需要每一次循环都置零,保留原值即可。
- 还有些同学可能会疑惑,for循环内加了个while循环,为什么时间复杂度还是O(n)呢?这是由于,如果考虑最坏情况,每个元素都是在滑动窗口后进来一次,出去一次,一共2n次,时间复杂度即O(n)