文章目录
- 一、非滑动窗口
- 1.1 560/LCR 010. 和为 K 的子数组
- 1.2 862. 和至少为 K 的最短子数组
- 二、滑动窗口
- 2.1 209/LCR 008. 长度最小的子数组
- 2.2 220. 存在重复元素 III
下面的题并不是全都由滑动窗口解决,有的题可以,有的题不可以,放入滑动窗口栏目的原因是,这些容易混淆。
一、非滑动窗口
当数组元素有负数时,不能使用滑动窗口,因为右指针右移可能使答案减小,也可能增大,左指针的移动同理,无法有效进行移动。
1.1 560/LCR 010. 和为 K 的子数组
LeetCode:LCR 010. 和为 K 的子数组
由于它需要找到的时连续子数组和为k
的个数,也就是说看连续子数组中有多少个区间和刚好为k
。我们观察到这里的nums[i]
存在负数,因此使用双指针滑动窗口不太好判断左右指针的移动方向,因为右指针右移可能使答案减小,也可能增大,左指针的移动同理。
因此我们可以尝试使用前缀和,那么只需要找到两个数,其差为k
即可,这使用哈希表就能解决。
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
int sum = 0;
int ans = 0;
mp[0] = 1;
for(int i = 0; i < nums.size(); ++ i){
sum += nums[i];
if(mp.count(sum - k)) ans += mp[sum - k];
mp[sum] ++;
}
return ans;
}
};
需要注意的是,需要初始化mp[0] = 1
,我们要理解前缀和sum[i] - sum[j]
表示的和的范围,范围为[j+1,i]
;那么我们要能够表示[0,i]
那就必须出现sum[i] - sum[-1]
,实际上这里就是用sum[-1]=0
来表示没有任何元素的时候的和。如果不加入这个和为0
的值,则可能缺失答案。
1.2 862. 和至少为 K 的最短子数组
LeetCode: 862. 和至少为 K 的最短子数组
本题和LeetCode:LCR 008. 长度最小的子数组不一样之处在于,本题存在负数,一旦存在负数,滑动窗口就不好使了。
本题和LeetCode:LCR 010. 和为 K 的子数组的区别有两点,第一点是这里求最小,第二点是这里和是至少为k
。
求最小我们自然而然可以想到前缀和时同一个数最右边那个效益最大,和至少为k
导致不能使用哈希表,且不能使用二分查找,因为存在负数,前缀和有正有负。
那怎么办呢?哈希表和二分查找都不能用。 但是前缀和是要用的,毕竟区间问题转化成了两点问题。转化成前缀和后通过分析,我们有这样一个信息,同一个数越右边的数越有效;右边越小的数比左边更大的数有效,因为sum[i] - sum[j]
,sum[j]
越小,这个差值越大,即区间和越大,而且右边的小数区间也更短,也就是说现在有两个信息了:
(1)使用前缀和
(2)右边的小数比左边的更大数或相等数 是更有效的,因为此时既更能满足条件,区间又更短
因此这个题的数具有“时效性”,我们可以考虑使用单调队列
,我们遇到比之前的某些数更小的数一定是更有意义,因此放入队列,构成一个单调递增队列,不断弹出队尾元素,直到没有比它更小或等于的。我们保留比它小的原因是,可能有的数用刚加入的它不能满足条件,但是用比它更小的但是出现在之前的能满足条件。不过我们通过引入单调队列
必然就使得每次加入都是有效的数,无效的都被剔除了。而且单调队列
维护的是单增队列,可以使用二分查找
找到属于它的合法值。
前缀和+单调队列+二分查找:
时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
class Solution {
public:
int shortestSubarray(vector<int>& nums, int k) {
vector<long long> deque;//单增
vector<int> place;
deque.push_back(0);
place.push_back(-1);
long long sum = 0;
int len = 0x3f3f3f3f;
for(int i = 0; i < (int) nums.size(); ++ i){
sum += nums[i];
//查找是否有满足条件的值
long long target = sum - k;// sum - target >= k 则sum - k >= target,找小于等于target的数
auto it = upper_bound(deque.begin(), deque.end(), target);
if(it != deque.begin()){//找到合适位置,当为begin时,没有小于等于target的
-- it;
len = min(len, i - place[it - deque.begin()]);
}
while(!deque.empty() && sum <= deque.back()){
deque.pop_back();
place.pop_back();
}
deque.push_back(sum);
place.push_back(i);
}
return len == 0x3f3f3f3f ? -1 : len;
}
};
我们可以进一步优化时间复杂度:
前缀和 + 单调双端队列:
时间复杂度:
O
(
n
)
O(n)
O(n)
这里引入了一个新的问题,也就是说我们在考虑sum[i]
时,对于在单调队列中的元素sum[j]
来说,一旦存在sum[i] - sum[j] >= target
,那么sum[j]
的任务就完成了就可以弹栈了,为什么呢?因为我们找到了[j+1,i]
满足要求,对以j+1
为左端点的区间,i
是第一个满足条件的,i
之后位置再有[j+1,i+x]
满足题设条件时,区间长度已经不及[j+1,i]
了,也就是说一旦出现一个i
使得单调队列中元素满足条件,则这个元素可以功成名退。
因此我们可以直接考虑单调队列的队首,队首一旦满足条件就都能进行出队,因此进队出队一次,时间复杂度为
O
(
n
)
O(n)
O(n)。
class Solution {
public:
int shortestSubarray(vector<int>& nums, int k) {
deque<long long> deq;//单增
deque<int> place;
deq.push_back(0);
place.push_back(-1);
long long sum = 0;
int len = 0x3f3f3f3f;
for(int i = 0; i < (int) nums.size(); ++ i){
sum += nums[i];
//查找是否有满足条件的值
while(!deq.empty() && sum - deq.front() >= k){
len = min(len, i - place[0]);
deq.pop_front();
place.pop_front();
}
while(!deq.empty() && sum <= deq.back()){
deq.pop_back();
place.pop_back();
}
deq.push_back(sum);
place.push_back(i);
}
return len == 0x3f3f3f3f ? -1 : len;
}
};
二、滑动窗口
2.1 209/LCR 008. 长度最小的子数组
LeetCode:LCR 008. 长度最小的子数组
这个题可以使用滑动窗口,主要原因是,数组中全为正数,这样一来,当右指针右移时,连续子数组的和增加,左指针右移时,连续子数组的和减小。
为了找到所有可能满足条件的子数组,我们需要对滑动窗口不断伸缩,对于一个已经满足要求的滑动窗口而言,其右指针不动的情况下不能再找到满足条件的最小子数组,因为都为正数,右指针不变窗口变小不满足和大于等于target
的要求,右指针不能左移因为你是通过右移得到这个位置的,左移肯定不行;右指针右移不满足要求,因为值变大但长度也变大。此时可能满足要求的情况还有,左指针右移到某个位置,以该位置为起点的滑动窗口。
那么我们左指针从0
开始,右指针右移一直到和≥target
或到数组尾部,如果和≥target
的话,记录长度,然后左指针右移前往寻找下一个可能满足条件的滑动窗口左端点,一旦右移到<target
则为可能的答案。
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0;
int right = 0;
int slidewindow = nums[0];
int len = 0x3f3f3f3f;
while(right < nums.size()){
if(slidewindow < target){
right ++;
if(right < nums.size()) slidewindow += nums[right];
}
if(slidewindow >= target){
len = min(len, right - left + 1);
if(right == left) break;//滑动窗口最短情况的临界条件
slidewindow -= nums[left ++];
}
}
return len == 0x3f3f3f3f ? 0 : len;
}
};
当然本题也能使用前缀和+二分查找,由于本题要求的是大于等于而不是等于,因此不能用哈希表,但是由于前缀和恒增,所以可以通过二分查找来查找指定值cur_sum - target
或小于它的第一个数
对比一下官解的解决方法:
官解保证循环每一次都更新一次右指针位置,相当于循环内部每次需要将左指针移动到正确位置,这里正确位置包括和还没达到要求,左指针不动,和达到要求,左指针往右移动到正确位置。注意这里保证右指针所到之处才加值。这样的话左指针大于右指针的时候和为0,一定下一次可以让右指针右移
而我的思路是循环内部只移动一次,可能左指针右移或右指针右移,具体情况看和的大小。
int ans = INT_MAX;
int start = 0, end = 0;
int sum = 0;
while (end < n) {
sum += nums[end];
while (sum >= s) {
ans = min(ans, end - start + 1);
sum -= nums[start];
start++;
}
end++;
}
2.2 220. 存在重复元素 III
LeetCode:220. 存在重复元素 III
本题有两种方法实现。
滑动窗口+哈希二分
在使用滑动窗口的方法中,我们维护一个indexDiff
大小的滑动窗口,和209. 长度最小的子数组、862. 和至少为 K的最短子数组不同,由于这里没有区间大小求解限制,而且这里是两个元素之间的关系,因此元素存在负数没关系,对于新加入的每一个数,我们只需要找到满足条件的元素即可。
这里我们考虑是否能让滑动窗口里面的元素动态排序,然后还可以动态删除? 哈希表map
和set
能做到这一点,而且他们也能进行二分查找使用
O
(
l
o
g
n
)
O(logn)
O(logn)的时间找到所需要的答案。
class Solution {
public:
bool containsNearbyAlmostDuplicate(vector<int>& nums, int indexDiff, int valueDiff) {
map<int, int> st;
int left = 0, right = 0;
while(right < (int) nums.size()){
//right是当前要考虑的元素
int target1 = nums[right] - valueDiff;
int target2 = nums[right] + valueDiff;
//查看有没有在 [target1,target2]内的元素
//查看有没有数大于等于target1
auto it = st.lower_bound(target1);
if(it != st.end() && it->first <= target2) return true;
//查看有没有数小于等于target2
it = st.upper_bound(target2);
if(it != st.begin()){
it--;
if(it->first >= target1) return true;
}
st[nums[right ++]] ++;
if(right - left > indexDiff){
if(-- st[nums[left]] == 0) st.erase(nums[left ++]);
}
}
return false;
}
};