1. 算法思想
滑动窗口的本质是:维护一个满足条件的连续子数组/子串,通过移动左右边界来"滑动"这个窗口,从而找到最优解。 滑动窗口是更加严格的双指针算法,大致思路都是用两个不回退的指针维护窗口。而且滑动窗口仅支持元素为正数的情况,不适用于负数或0(出窗口的时机无法确定)。
基本步骤: 进窗口 -> 判断条件 -> 出窗口 -> 更新结果
(更新结果的时机因题而异)
解题的关键在于为什么而不是怎么做,因为大多数情况不是不会用而是想不到用滑动窗口,因此思考暴力解法的优化过程才是重中之重。
2. 经典例题
2.1 将 x 减到 0 的最⼩操作数
解题思路:
如果采用正面的思路解决,那么需要使用两个指针向中间移动,很难控制移动的先后与步数;因此我们可以试着采用逆向思维:既然x的加数都源于数组的两端,那么sum - x的加数一定是数组中的一段连续区间。
有了连续区间,又有了固定的和作为区间的维护条件,而且数组的元素不存在非负数,此时就是熟悉的滑动窗口问题了。
- 滑动窗口
O(N):设sum - x为target,当窗口中的和小于target,进窗口;大于target,反复出窗口;然后更新结果。由于求的是和为x的最小操作数,那么和为target的区间长度就要最大。
优化点 :如果数组中所有元素加起来都比x小,则可以直接返回false。

计算target需要遍历一次数组,滑动窗口的过程也会遍历一次数组,因此时间为N + N,时间复杂度为O(N)。
cpp
class Solution {
public:
int minOperations(vector<int>& nums, int x)
{
int left = 0, right = 0, minlen = INT_MAX;
long long sum = 0;
int arrsum = 0;
int n = nums.size();
// 求和
for (auto i : nums)
{
arrsum += i;
}
long long key = arrsum - x;
if(key < 0) return -1; //所有的数加起来都比x小,则不成立(滑动窗口仅支持大于零的元素)
while (right < n)
{
sum += nums[right];
while (sum > key) //注意>的判断要在==前
{
sum -= nums[left++];
}
if (sum == key)
{
int len = n - (right - left + 1);
minlen = min(minlen, len);
}
right++;
}
return minlen == INT_MAX ? -1 : minlen;
}
};
2.2 水果成篮
解题思路:
- 暴力枚举+哈希表
O(N^2):枚举出所有的子数组,用哈希表存储子数组中树的种类,最后得出收集水果的最大数目。 - 滑动窗口+哈希表
O(N):由于是连续采摘,我们可以使用滑动窗口算法,并让窗口满足内部的水果种类仅有两种;用哈希表存储篮子中水果的种类。当种类小于等于两种时,进窗口;大于两种时,反复出窗口;最后更新结果。
注意点 :走过一个下标只能摘一个水果,数组元素大小表示的是水果种类!

cpp
class Solution {
public:
int totalFruit(vector<int>& f)
{
unordered_map<int, int> hash; // 统计窗口内出现了多少种水果
int ret = 0;
for (int left = 0, right = 0; right < f.size(); right++)
{
hash[f[right]]++; // 进窗口
while (hash.size() > 2)
{ // 判断
// 出窗口
hash[f[left]]--;
if (hash[f[left]] == 0)
hash.erase(f[left]);
left++;
}
ret = max(ret, right - left + 1);
}
return ret;
}
};
优化点 :由于操作涉及哈希表的插入和删除,耗时较大。为了优化时间复杂度,可以用数组模拟哈希表 (空间换时间),并用kinds变量记录窗口中的有效种类。
ps:有效种类的含义为,若新水果进窗口,则kinds++;若该种类的水果已全部离开窗口,则kinds--。
cpp
class Solution {
public:
int totalFruit(vector<int>& f)
{
int hash[100001] = {0}; // 用数组统计窗口内每种水果出现的次数
int ret = 0;
for (int left = 0, right = 0, kinds = 0; right < f.size(); right++)
{
if (hash[f[right]] == 0) kinds++; // 维护水果的种类数
hash[f[right]]++; // 进窗口
while (kinds > 2)
{ // 判断:如果超过两种水果
// 出窗口
hash[f[left]]--;
if (hash[f[left]] == 0) kinds--;
left++;
}
ret = max(ret, right - left + 1);
}
return ret;
}
};
2.3 找到字符串中所有字⺟异位词
解题思路:
快速判断某个子串是否是异位词子串的方式,可以用排序 或者哈希表。但是每次枚举一个新的子串就会产生排序,耗时太大;因此使用哈希表存储和比对。
- 暴力枚举+两个哈希表
O(N^2):一个哈希表存储给定子串中的字符,从每个元素开始向后暴力枚举所有子串,并用另一个哈希表存储,通过对比两个哈希表是否相同来找出所有子串。 - 滑动窗口+两个哈希表
O(N):一个哈希表存储给定子串中的字符,用固定长度的滑动窗口遍历数组,并用另一个哈希表存储窗口中的元素,通过对比两个哈希表是否相同来找出所有子串。
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p)
{
unordered_map<char, int> hash1;
unordered_map<char, int> hash2;
vector<int> ret;
for(auto ch : p)
{
hash2[ch]++;
}
int m = p.size();
for(int left=0, right=0; right < s.size(); right++)
{
hash1[s[right]]++;
while(right - left + 1 > m)
{
hash1[s[left]]--;
if(hash1[s[left]] == 0)
{
hash1.erase(s[left]);
}
left++;
}
if(right - left + 1 == m && hash1 == hash2)
{
ret.push_back(left);
}
}
return ret;
}
};
优化点 :用两个哈希表辅助计算字符种类,空间复杂度过高,因此可以用两个数组模拟哈希表实现。数组hash2用于存储给定子串中的字符种类和个数,数组hash1和变量count用来计算窗口中的"有效字符"。(数组大小均为26)
注意点 :数组的下标是ch - 'a'!


cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p)
{
int hash1[26] = {0};
int hash2[26] = {0};
vector<int> ret;
for(auto ch : p)
{
hash2[ch - 'a']++;
}
int m = p.size();
int count = 0;
for(int left=0, right=0; right < s.size(); right++)
{
//进窗口
hash1[s[right] - 'a']++;
if(hash1[s[right] - 'a'] <= hash2[s[right] - 'a'])
{
count++;
}
//出窗口
if(right - left + 1 > m)
{
if(hash1[s[left] - 'a'] <= hash2[s[left] - 'a'])
{
count--;
}
hash1[s[left] - 'a']--;
left++;
}
//更新结果
if(count == m)
{
ret.push_back(left);
}
}
return ret;
}
};
// 本期内容就到这里,如果对你有帮助,请三连支持!我是青云,我们下期见^_~