理解:滑动窗口是双指针的一种应用,本质上是维护了一段数据区间,区间的左右端点移动是单调的。具体含义是:一个指针移动,另一个指针只能往一个方向移动,不能两个方向来回移动。 一般是去解决一些子数组,子串的问题的。
实质:通过发现题目的一些单调性质,对暴力循环的一种简化,时间复杂度从平方降到一次方。
实现:可以基于双端队列,也可以基于左右指针,优先考虑左右指针更节省空间,但是如果需要对窗口中的元素进行一定的处理操作,那么选择双端队列实现。左右指针是通过左指针代表左边界,右指针代表右边界,二者同时向右移动的基础原理。双端队列是左端点出数,右端点进数,不断的将元素入队和出队来实现的。
题目:
力扣209 :长度最小的子数组
如果我们用暴力做法去做的话,我们需要去分别枚举子数组的左右端点。但我们可以发现很多子数组是没有必要去枚举的。例如:如果一个子数组的和都已经超过了tar了,我们还往后枚举,其实已经没有必要了。
我们可以观察到一个二段性质:子数组的和 ① >=tar ② <tar 依据此性质可以建立滑动窗口。
code:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int ans = INT_MAX;
int sum = 0;
for(int i = 0,j = 0;j<nums.size();j++)
{
sum += nums[j];
while(sum>=target)
{
ans = min(j-i+1,ans);
sum -= nums[i];
i++;
}
}
return ans == INT_MAX? 0 :ans;
}
};
细节: 为什么用while不用if? while也可以起到if的作用,而且是连续性的if。如果只有if,可能会导致只删除了一个数以后,子数组的和还是满足条件。
力扣 713: 乘积小于k的子数组
跟上一题一样的思路,只是特判一下边界即可。
code:
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if(k<=1) return 0;
int sum = 1 ;
int ans = 0;
for(int i = 0,j = 0;j<nums.size();j++)
{
sum*=nums[j];
while(sum>=k)
{
sum/=nums[i];
i++;
}
ans += j-i+1;
}
return ans;
}
};
细节: 固定了右端点以后,[l,r]有效的集合怎么算?[l-1,r]...[r,r]都是有效的集合,所以就是r-l+1
力扣3 无重复字符的最长子串:
怎么记录每个字母出现的次数?用哈希表
code:
class Solution {
public:
int map[1000];
int lengthOfLongestSubstring(string s) {
int n = s.size();
int ans = 0;
for(int i = 0,j = 0;j<n;j++)
{
map[s[j] ]++;
while(map[s[j]] > 1)
{
map[s[i]]--;
i++;
}
ans = max(ans,j-i+1);
}
return ans;
}
};
细节:这里用数组模拟哈希表了,原理是记录字符的ASCII值。
力扣904: 水果成篮
也是用滑动窗口就可以解决了的问题。但这里用数组模拟就不太可行了,我们用unordered_map去记录每种水果出现的次数就行了。最后判断map.size()是否大于2即可。
code:
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int,int> mp;
int n = fruits.size();
int ans = 0;
for(int i = 0,j = 0;j<n;j++)
{
mp[fruits[j]]++;
while(mp.size()>2)
{
mp[fruits[i]]--;
if(mp[fruits[i]] == 0) mp.erase(fruits[i]);
i++;
}
ans = max(ans,j-i+1);
}
return ans;
}
};
细节: unordered_map加入元素可以用mp[i]++,如果原来没有i这个key,会自动加上。删除元素如果只是mp[i]--,那么只会删除key对应的value的值,删除key要用到erase函数。
力扣76: 最小覆盖子串
这个题目需要记录的窗口中的子串里面需要有t串中所有的元素,其实种类数量都需要对应上。这不是正好可以用哈希表去解决吗
我们可以开两个哈希表,一个是s中出现的元素种类与个数,另一个记录t中出现的种类与个数。
另外还得开一个cnt变量,去记录当前窗口中满足t串元素的个数。因为如果当前s中只有t串元素的子集,那是不能更新答案的。
右端点前进,元素放入s哈希表中,如果遇到了t中的元素,这个时候我们需要增加cnt。因为这是第一次遇见,所以这个元素一定是需要的。
同时我们需要考虑左端点的值,如果左端点的对应的值已经超过t中某元素的数量上限,我们需要剔除该元素并前移一位。
当cnt == t.size()的时候,说明我们可以收集答案了。答案就是ans与当前窗口长度的最小值。
code:
class Solution {
public:
int cnt;
string minWindow(string s, string t) {
unordered_map<char,int> mps,mpt;
string ans;
for(auto i:t) mpt[i]++;
for(int i = 0,j = 0;j<s.size();j++)
{
mps[s[j]]++;
if(mps[s[j]]<=mpt[s[j]]) cnt ++;
while(mps[s[i]]>mpt[s[i]])
mps[s[i++]]--;
if(cnt == t.size())
{
if(!ans.size()||j-i+1<ans.size())
ans = s.substr(i,j-i+1);
}
}
return ans;
}
};
acwing 3624 三值字符串
也是一道模板题。关键是记录元素的种类已经个数。当元素个数到达3个的时候开始考虑移动左端点。
code:
#include<iostream>
#include<algorithm>
#include<unordered_map>
using namespace std;
unordered_map<char,int> mp;
int main()
{
int m;
cin>>m;
while(m--)
{
mp.clear();
string s;
cin>>s;
int ans = 2*1e6;
for(int i = 0,j = 0;j<s.size();j++)
{
mp[s[j]]++;
while(mp[s[i]]>1 &&mp.size()==3) mp[s[i++]]--;
if(mp.size()==3)ans = min(ans,j-i+1);
}
if(ans == 2*1e6) ans =0;
cout<<ans<<endl;
}
return 0;
}
力扣187:重复的DNA序列
按题意直接模拟一个窗口。
code:
class Solution {
public:
vector<string> ans;
unordered_map<string,int> map;
vector<string> findRepeatedDnaSequences(string s) {
if(s.size()<=10) return ans;
for(int l = 0,r = 9;r<s.size();r++)
{
while(r-l+1>10) l++;
string str = s.substr(l,r-l+1);
map[str]++;
if(map[str]==2) ans.push_back(str);
}
return ans;
}
};
力扣219:存在重复元素Ⅱ
感觉跟滑动窗口很像,但本质就是用哈希记录每个端点出现的下标。
code: insert是为了方便处理,下标从1开始。
class Solution {
public:
unordered_map<int,int> mp;
bool containsNearbyDuplicate(vector<int>& nums, int k) {
nums.insert(nums.begin(),0);
int n = nums.size();
for(int i = 1;i < n;i++)
{
if(mp[nums[i]])
{
if(i - mp[nums[i]]<=k)
return 1;
}
mp[nums[i]] = i;
}
return 0;
}
};
力扣395:至少有k个重复字符的最长子串
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
这题很特殊,由于我们不知道区间可能会出现几种字母,所以我们很难用双指针。但是考虑到一共就26个字母,所以我们直接26次暴力枚举一下,然后每种情况滑动窗口一下就行了
- 右端点往右移动必然会导致字符类型数量增加(或不变)
- 左端点往右移动必然会导致字符类型数量减少(或不变)
当遇到新元素,diff_cnt++,否则就不是新元素,那该元素的cnt++;当我们遇到的元素种类大于i的时候,开始缩小窗口。
code:
class Solution {
public:
int map[26];
int longestSubstring(string s, int k) {
int ans = 0;
for(int i = 1;i<=26;i++)
{
memset(map,0,sizeof (map));
int diff_cnt = 0,cnt = 0;
for(int l = 0, r = 0;r<s.size();r++)
{
int add_idx = s[r] - 'a';
map[add_idx]++;
if(map[add_idx] == 1) diff_cnt++;
if(map[add_idx] == k) cnt++;
while(diff_cnt > i)
{
int del_idx = s[l] - 'a';
if(map[del_idx]==k) cnt--;
if(map[del_idx]==1) diff_cnt--;
map[del_idx]--;
l++;
}
if(diff_cnt == i && diff_cnt == cnt)
ans = max(ans,r-l+1);
}
}
return ans;
}
};
力扣438,LCR015: 找到字符串中的所有字母异位词
这里要维护的集合需要额外的两个变量,一个是需要的字符数量cnt,另一个是目前多余的字符数量diff_cnt。用两个哈希表去记录,一个记录字符串p中出现的元素,另一个记录当前窗口中出现的元素次数。
当窗口右移的时候,哈希表加入当前的字符,先判断是否这个字符是否是需要的字符,如果满足maps[s[j]] <= mapp[s[j]],说明是需要这个字符的,如果不满足这个条件,有两个可能:第一:需要字符的种类已经超出需要的数目了,第二:出现了不需要字符的种类。这个时候diff_cnt就要加了。
左指针移动的条件是:当左指针指向的字符的数目已经大于需要的字符数目的时候,就要移动了。移动的时候要记得删除cnt与diff_cnt的数目。
收集结果:必须满足cnt == p.size()且diff_cnt == 0,说明当前维护的区间里面的值没有多余的字符。
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> ans;
int n = s.size(),m = p.size();
if(n<m) return ans;
unordered_map<char,int> maps,mapp;
for(auto i:p) mapp[i]++;
int cnt = 0,diff_cnt = 0; //cnt当前已经有的目标字符个数,diff_cnt表示窗口内的
for(int i = 0,j = 0;j<n;j++)
{
maps[s[j]]++;
if(maps[s[j]] <= mapp[s[j]]) cnt ++;
else diff_cnt++;
while(maps[s[i]]>mapp[s[i]])
{
if(maps[s[i]] > mapp[s[i]]) diff_cnt--;
maps[s[i++]]--;
}
if(cnt == p.size()&&diff_cnt == 0)
ans.push_back(i);
}
return ans ;
}
};
力扣930,2799:和相同的二元子数组 统计完全子数组的数目
关键点就是当我们找到满足的数组以后,包括该数组的左边数组其实都是满足要求的!
所以我们移动左端点的原则就是让当前区间是最小的合法区间。那必须是要满足两个条件:第一:区间里面数的种类达到k个,第二就是每个数的数量必须为1。所以是mp.size() == k && mp[nums[i]]>1。
统计结果的时候我们也需要满足mp.size() == k。
class Solution {
public:
// 左边的数都是满足要求的!!!
int countCompleteSubarrays(vector<int>& nums) {
unordered_set<int> st(nums.begin(),nums.end());
int k = st.size();
unordered_map<int,int> mp;
int ans = 0;
for( int i = 0,j=0;j<nums.size();j++)
{
mp[nums[j]]++;
while(mp.size() == k && mp[nums[i]]>1)
{
mp[nums[i++]]--;
}
if(mp.size() == k)ans += i+1;
}
return ans;
}
};
力扣2260: 必须拿起的最小连续卡牌数
这个比较模板,只需要记录一下最小的答案就行了。判断条件就是右指针指向的数出现了两次,那就可以缩小区间了,最后那个区间长度就是答案。
class Solution {
public:
int map[1000005];
int minimumCardPickup(vector<int>& cards) {
int ans = INT_MAX;
int n = cards.size();
for(int i = 0,j = 0;j<n;j++)
{
map[cards[j]]++;
while(map[cards[j]]>1)
{
ans = min(ans,j-i+1);
map[cards[i++]]--;
}
}
return ans == INT_MAX?-1:ans;
}
};
力扣1234: 替换子串得到平衡字符串
反向思维:先记录所有字符出现的次数。
题目都很难读懂。 意思就是把分成两个区间,一个是可替换区间,这个以外的是不可替换区间。只有当不可替换区间的每种字母数目不超过m/4才可以用可替换区间里面的字符去变化。所以我们左端点移动的条件就是每种字母数目不超过m/4。
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
class Solution {
public:
int map[1000];
int balancedString(string s) {
int n = s.size(), m = n/4;
for(auto i: s) map[i]++;
int ans = n;
if(map['Q'] == m && map['W'] == m && map['E'] == m && map['R'] == m) return 0;
for(int i = 0,j = 0;j<n;j++)
{
map[s[j]] -- ;
while(map['Q'] <= m && map['W'] <= m && map['E'] <= m && map['R'] <= m)
{
ans = min(ans,j-i+1);
map[s[i++]]++;
}
}
return ans;
}
};
力扣1456:定长子串中的元音的最大数目
模拟一个滑动窗口就可以了,哈希表记录窗口里面的元音的次数。
class Solution {
public:
int map[1000];
int maxVowels(string s, int k) {
int n = s.size();
if(n<k) return 0;
int ans = 0;
for(int i = 0;i<k-1;i++) map[s[i]]++;
for(int i = 0,j = k-1;j<s.size();j++)
{
map[s[j]]++;
while(j - i > k -1)
map[s[i++]]--;
int sum = map['a'] + map['e']+map['i']+map['o']+map['u'];
ans = max(sum,ans);
}
return ans;
}
};
力扣1658:将x减到0的最小操作数
逆向思维:只能从左或者右减去,所以最后的答案必然是原数组的一个子数组。所以问题转换成找到原数组的一个子数组的和为x,求子数组最大长度。
accmulate:(pos,pos,x)x为初始累加值。
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int tar = accumulate(nums.begin(),nums.end(),0) - x;
if(tar<0) return -1;
int ans = -1, n = nums.size(),sum = 0;
for(int i = 0,j = 0;j<n;j++)
{
sum+=nums[j];
while(sum>tar)
sum-=nums[i++];
if(sum == tar) ans = max(ans,j-i+1);
}
return ans<0?-1:n-ans;
}
};
力扣2762:不间断子数组
分析完题目其实就是记录三个值:集合中的最大值,最小值,跟即将加进来的数。
我们可以用multiset(红黑树)去维护。
每次加入multiset中,由于是自动排序的,最大值就是st.rbegin(),最小值是st.begin()。如果当前这个数的加入让最大值与最小值之差小于2了,那么我们就要开始移动左指针了。直到满足条件即可。
最后的答案就是区间长度。
code: 注意: 最后一个元素的指针不是end(),是rbegin()。
erase可以接受值或者是指针。如果接受的是指针,删除的是该指针指向的值,如果是值,那multise中所有该值都会被删除。
与单调栈,单调队列的区别:这里的区间内元素顺序不是固定的,所以我们可以用multiset,如果元素顺序固定就要用单调队列了。
class Solution {
public:
long long continuousSubarrays(vector<int>& nums) {
long long ans = 0 ;
multiset<int> st;
int n = nums.size();
for(int i = 0,j = 0;j<n;j++)
{
st.insert(nums[j]);
while(*st.rbegin() - *st.begin() > 2)
st.erase(st.find(nums[i++]));
ans += j -i+1;
}
return ans;
}
};
一些关于哈希表的补充: