每日一算法——双指针; leetcode LCR 008.长度最小的子数组
典型的双指针算法, 虽然看起来是两层循环, 实际上有很多优化.
双指针
双指针方法较为灵活, 很难说哪些场景固定使用. 但一般像滑动窗口、两层 for 循环仅控制下标等都能变为双指针. 直接通过题目来学习吧.
Leetcode LCR 008.长度最小的子数组
给定一个含有 n 个正整数的数组 nums
和一个正整数 target
.
找出该数组中满足其和 $\ge$ target
的长度最小的 连续子数组 numsl, numsl+1, ..., numsr-1, numsr
, 并返回其长度.
如果不存在符合条件的子数组, 返回 0 .
- 输入: target = 7, nums = [2,3,1,2,4,3]
- 输出: 2
- 解释: 子数组 [4,3] 是该条件下的长度最小的子数组.
Brute Force
最 Brute 的思路, 控制两层 for
循环分别控制下标, i
控制起始, j
控制从 i
开始之后的每一个数,
求和, 然后每次更新满足条件的最小长度即可. 下面是个大致的代码思路, 没运行测试过, 可能有误.
这里我们让 len
先等于最大的 int
, 方便最后判断是否存在一个子数组满足大于等于 targer
.
int minSubArrayLen(int target, vector<int>& nums) {
const int N = nums.size();
int len = INT_MAX;
int sumup = 0;
for(int i = 0; i < N; i++) {
sumup = 0;
for( int j = i; j < N; j++) {
sumup += nums[j];
if (sumup >= target) {
len = min(len, j - i + 1);
break;
}
}
}
return len == INT_MAX ? 0 : len;
}
对于上面的代码, 实际上我们很容易就知道有大量的重复工作, 例如从 i=2, j=2
求一遍, 然后 i=3, j=3
求一遍,
实际在反复地遍历 j < N
的部分, 特别是当不存在求和成立的时候. 平均时间复杂度 $O(n^2)$ .
使用双指针
实际上, 我们可以维护一个"滑动窗口", 每次计算滑动窗口内的和, 当和小于 target
时扩大, 反之则尝试缩小.
而关键的点在于如何缩小或移动这个滑动窗口才能减少时间复杂度.
一个想法是, 当我们有一定大小的滑动窗口且满足 target
的大小条件时, 我们不收缩最右侧的位置,
而是从左侧开始收缩直到当前的和不再满足大小条件, 则可以开始将右指针继续向右移动. 这个收缩的过程同样需要避免反复求和,
即我们可以将之前已经得到的和减去收缩被丢弃的数.
int minSubArrayLen(int target, vector<int>& nums) {
const int N = nums.size();
int len = INT_MAX;
int sumup = 0;
int left = 0, right = 0;
while (right < N) {
sumup += nums[right];
while (sumup >= target) {
len = std::min(right - left + 1, len);
sumup -= nums[left];
left++;
}
right++;
}
return len == INT_MAX ? 0 : len;
}