cpp
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int left = 0;
int right = numbers.size() - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
return {left + 1, right + 1};
} else if (sum < target) {
++left;
} else {
--right;
}
}
return {};
}
};
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size();
vector<long long> prefix(n + 1, 0);
for (int i = 0; i < n; ++i)
prefix[i + 1] = prefix[i] + nums[i];
deque<int> dq;
int res = INT_MAX;
for (int i = 0; i <= n; ++i) {
while (!dq.empty() && prefix[i] - prefix[dq.front()] >= target) {
res = min(res, i - dq.front());
dq.pop_front();
}
while (!dq.empty() && prefix[dq.back()] >= prefix[i]) {
dq.pop_back();
}
dq.push_back(i);
}
return res == INT_MAX ? 0 : res;
}
};
双指针与单调队列两道典型题整理
一、Two Sum II(有序数组的两数之和)------双指针
题意简述
给定一个递增有序数组 numbers,在其中找出两个数,使得它们的和等于 target。返回这两个数的下标(1-based),且保证恰好存在一对解。
解法:双指针
有序数组 + 两数之和,是双指针的典型场景。
-
左指针
left指向开头 -
右指针
right指向末尾 -
计算
sum = numbers[left] + numbers[right]-
若
sum == target:直接返回下标(注意是 1-based) -
若
sum < target:说明和偏小,需要增大和 → 左指针右移 -
若
sum > target:说明和偏大,需要减小和 → 右指针左移
-
数组有序这一条件保证:
针对当前 left,所有右侧元素中,随着下标减小,和单调减小;针对当前 right,随着左指针增大,和单调增大。
因此每一步都可以排除一段不可能区间,整体时间复杂度 O(n)。
代码要点
-
使用
while (left < right),保证每对只枚举一次 -
返回
{left + 1, right + 1},符合题目 1-based 要求 -
若题目没有保证答案存在,可以循环结束后返回空数组
时间复杂度:O(n)
空间复杂度:O(1)
二、最短子数组长度(前缀和 + 单调队列)
对应函数:
cpp
int minSubArrayLen(int target, vector<int>& nums);
这里的实现不是经典"全正数 + 滑动窗口"的版本,而是前缀和 + 单调队列版本,对负数也适用,如果就是这道题的话,就定义一个sum存left到right的全部和就行,大于就移动left,小的话就移动right。
1. 前缀和构造
定义前缀和数组:
cpp
prefix[0] = 0;
prefix[i+1] = prefix[i] + nums[i]; // i = 0..n-1
prefix[i] 表示前 i 个元素之和。
任意区间 [l, r) 的和为:
cpp
sum(l, r) = prefix[r] - prefix[l];
问题转化为:
找一对下标 (j, i),满足
cpp
prefix[i] - prefix[j] >= target
且 i - j 尽量小。
i 从左到右扫描,对每个 i,需要找最小的 i - j,同时满足上式。
2. 单调队列设计
使用一个双端队列 dq,存储一组下标 j,并维护以下性质:
-
队列中的下标按递增顺序排列:
dq.front() < ... < dq.back() -
对应的前缀和值严格单调递增:
prefix[dq[0]] < prefix[dq[1]] < ... < prefix[dq.back()]
理由:
-
对当前
i,若想让区间[j, i)的和尽量大,优先使用更小的前缀和prefix[j] -
若存在
j1 < j2,且prefix[j1] >= prefix[j2],那么对之后所有
i:-
用
j1不会比用j2更好(区间更长,和更小或相等) -
因此
j1可以从队列中丢弃
-
3. 扫描过程
对 i 从 0 到 n 遍历(注意前缀和长度为 n+1):
-
尝试更新答案(用当前前缀和
prefix[i]作为右端):cppwhile (!dq.empty() && prefix[i] - prefix[dq.front()] >= target) { res = min(res, i - dq.front()); dq.pop_front(); }这一步含义:
-
队首始终是当前可用中最早、且前缀和最小的下标
-
若已经满足
prefix[i] - prefix[dq.front()] >= target,说明
[dq.front(), i)是一个合法区间 -
为了找到更短区间,持续弹出队首
-
-
维护单调性(保证队列里的
prefix单调递增):cppwhile (!dq.empty() && prefix[dq.back()] >= prefix[i]) { dq.pop_back(); } dq.push_back(i);这一步是在保证:
-
对未来的任何
i2 > i,若同时存在:-
较旧下标
j_old = dq.back() -
较新下标
j_new = i
-
-
且
prefix[j_old] >= prefix[j_new]则
j_old永远不会优于j_new,可以直接删掉。
-
整体时间复杂度 O(n),因为每个下标最多入队、出队一次。
4. 完整逻辑小结
-
利用前缀和将区间和问题转化为差值问题
-
双端队列按下标递增维护一个"前缀和单调递增"的序列
-
扫描到每个
i时:-
尝试从队首弹出所有能构成合法区间的下标并更新答案
-
再维护队列尾部的单调性,加入当前
i
-
时间复杂度:O(n)
空间复杂度:O(n)