本篇博客我们将深入学习经典的算法思想:滑动窗口
相关的习题博客已经上传至作者的个人gitee:CPP 学习代码库: C++代码库新库,旧有C++仓库满员了,喜欢请支持一下谢谢。
目录
[4、将x减到0 的最小操作数](#4、将x减到0 的最小操作数)
什么是滑动窗口算法
滑动窗口(Sliding Window)是一种经典的双指针算法思想,主要用于处理数组或链表的连续子序列问题。它通过维护一个窗口区间来高效地解决问题,避免了不必要的重复计算。
滑动窗口的基本思想是:维护一个窗口,通过移动窗口的边界来遍历数据。窗口的大小可以是固定的(固定窗口),也可以是变化的(动态窗口)。
滑动窗口的应用场景
滑动窗口算法特别适用于以下类型的问题:
- 子串/子数组问题:如"查找字符串中无重复字符的最长子串"
- 连续元素问题:如"寻找和大于等于目标值的最短连续子数组"
- 统计类问题:如"统计数组中满足特定条件的连续子数组个数"
滑动窗口的实现步骤
典型的滑动窗口算法实现步骤如下:
- 初始化窗口边界(通常是left=right=0)
- 扩展窗口(移动right指针)
- 当窗口满足条件时,收缩窗口(移动left指针)
- 在窗口移动过程中记录或更新结果
滑动窗口的变种
- 固定窗口:窗口大小保持不变,如"大小为K的子数组的最大平均数"
- 计数窗口:窗口大小由元素计数决定,如"包含最多K个不同字符的最长子串"
- 多指针窗口:使用多个指针维护复杂窗口条件
1、长度最小的子数组

思路:
解法一:暴力枚举所有子数组的和O(N^3)
解法二:利用单调性,使用同向双指针优化算法O(N^2)
揭发三:滑动窗口:
解法思路:
1、left、right=0;
2、进窗口
3、判断
4、出窗口。
其中2、3为循环操作
滑动窗口的正确性:利用了单调性规避了无效的枚举
答案:
cpp
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums)
{
int left=0,right=0;
int n=nums.size();
int len=INT_MAX;
int sum=0;
while(right<n)
{
sum+=nums[right];
while(sum>=target)
{
len=min(len,right-left+1);
sum-=nums[left++];
}
right++;
}
return len==INT_MAX?0:len;
}
};
2、无重复字符的最长字串

字串可以认为是子数组
解题思路1:暴力枚举+哈希表(判断是否有字符重复)
解题思路2:"滑动窗口"来解决问题
思路:
1、left=0,fight=0;
2、进窗口------字符进入哈希表
3、判断是否出现重复字符,如果出现就出哈希表(删除字符)
4、更新结果
答案:
cpp
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
int n=s.size();
int hash[128]={0};//用数组模拟哈希表
int left=0,right=0;
while(right<n)
{
hash[s[right]]++;//进入哈希表就是进入窗口
while(hash[s[right]]>1)//判断,>1出窗口
{
hash[s[left]]--;//出窗口
}
ret=max(ret,right-left+1);
right++;//下一个元素进窗口
}
}
};
3、最大连续1的个数三

解决思路:等价为在数组中找到最长子数组,满足0的个数小于k即可
算法一:暴力解法所有的子数组,再统计0的个数。
算法二:利用滑动窗口。
1、left=0,right=0;
2、进窗口如果是1无视,如果是0进窗口
3、判断.zero>k则非法。
cpp
class Solution {
public:
int longestOnes(vector<int>& nums, int k)
{
int ret = 0;
for (int left = 0, right = 0, zero = 0; right < nums.size(); right++)
{
if (nums[right] == 0)
{//进窗口
zero++;
}
while (zero > k)
if (nums[left++] == 0)
zero--;//出窗口
ret = max(ret, right - left + 1);//更新结果
}
return ret;
}
};
4、将x减到0 的最小操作数

算法思路:左右各选择一个区间,使中间元素相加为x。转化为找到最长的子数组长度。
滑动窗口:初始化left、right、sum为0 ,target=sum-x;进窗口,sum+nums[right];判断,sum>target的时候出窗口,即sum-nums[left];当sum=target时,更新结果
cpp
class Solution {
public:
int minOperations(vector<int>& nums, int x)
{
int left = 0, right = 0, tmp = 0;
int sum = 0;
for (auto a : nums) sum += a;
int target = sum - x;
//找不到
if (target < 0) return -1;
int ret = -1;
//滑动窗口
while (right < nums.size())
{
//进窗口
tmp += nums[right];
//判断
while (tmp > target)
tmp -= nums[left++];
if (target == tmp)//更新结果
ret = max(ret, right - left + 1);
right++;
}
return ret == -1 ? ret : nums.size() - ret;
}
};
5、水果成篮


转化为:找出最长子数组的长度,子数组中不超过两种类型的水果
算法一:暴力解法+哈希表
算法二:滑动窗口+哈希表
在left和right之间种类只有两种变化:不变或者变小。种类不变的时候right不变;种类变小的时候right向右。
滑动窗口:
1、初始化left、right为0
2、进窗口:将相同元素填入哈希表,+1
3、出窗口:哈希表的长度大于2,此时出窗口,left++
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits)
{//算法一
//unordered_map<int,int> hash;//统计窗口内多少钟水果
// int left=0,right=0;
// int ret=0;
// while(right<fruits.size())
// {
// hash[fruits[right]]++;//进窗口
// while(hash.size()>2)//出窗口
// {
// hash[fruits[left]]--;
// if(hash[fruits[left]]==0)
// hash.erase(fruits[left]);
// left++;
// }
// ret=max(ret,right-left+1);
// right++;
// }
// return ret;
//算法二:数组模拟哈希表
int hash[100001]={0};
int left=0,right=0;
int kinds=0;//水果种类
int ret=0;
while(right<fruits.size())
{
if(hash[fruits[right]]==0) kinds++;//维护水果种类
hash[fruits[right]]++;//进窗口
while(kinds>2)//出窗口
{
hash[fruits[left]]--;
if(hash[fruits[left]]==0)
kinds--;
left++;
}
ret=max(ret,right-left+1);
right++;
}
return ret;
}
};
6、找到字符串中所有的字符异位词

算法思路:滑动窗口+哈希表
如果快速判断两个字符串是否为异位词------哈希表
利用哈希表遍历字符串一,将每个字符出现次数记录,再遍历字符串二,出现相同字符的时候对应减去1,如果为0则证明为异位词。
利用变量count统计窗口中"有效字符"的个数优化
滑动窗口:
1、初始化left和right为0。准备两个哈希表hash1、hash2分别对应两个字符
2、进窗口。如果hash2[in]<=hash1[in],那么count++
3、判断出窗口。hash2[out]<hash1[out],则是有效字符,count--;其余情况下不用管count
4、更新结果:检查hash1和hash2的结果是否一致。
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p)
{
vector<int>ret;
int hash1[26]={0};//统计p中每个字符个数
int hash2[26]={0};//统计款口中字符出现的个数
for(auto& ch:p) hash1[ch-'a']++;
int left=0,right=0,count=0;
int m=p.size();
while(right<s.size())
{
char in=s[right];//进入窗口字符
hash2[in-'a']++;//进入窗口
if(hash2[in-'a']<=hash1[in-'a']) count++;
if(right-left+1>m)
{
char out =s[left++];
//出窗口
if(hash2[out-'a']<=hash1[out-'a']) count--;
hash2[out-'a']--;
}
//更新结果
if(count==m)
ret.push_back(left);
right++;
}
return ret;
}
};
7、串联所有单词的字串

这道题的思路与上一题类似,可以将words中的词当作一个字符对待。
区别:
1、哈希表
用hash<string ,int>
2、left、right
两者移动的步长是每个单词的长度
3、滑动窗口次数
cpp
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words)
{
unordered_map<string,int>hash1;//保存words中所有单词频次
vector<int> ret;
for(auto& e:words) hash1[e]++;
int len=words[0].size(),m=words.size();
//滑动窗口
for(int i=0;i<len;i++)//执行len次滑动窗口
{//count为有效字符串个数
unordered_map<string,int>hash2;//维护窗口内单词的频次
for(int left=i,right=i,count=0;right+len<=s.size();right+=len)
{
//进窗口
string in=s.substr(right,len);//
hash2[in]++;
//判断in是否在里面
if(hash1.count(in)&&hash2[in]<=hash1[in]) count++;
//判断
if(right-left+1>m*len)
{
//出窗口
string out=s.substr(left,len);
//判断out是否在里面
if(hash1.count(out)&&hash2[out]<=hash1[out]) count--;
hash2[out]--;
left+=len;
}
//更新维护
if(count==m) ret.push_back(left);
}
}
return ret;
}
};
8、最小覆盖字串

算法思路:滑动窗口+哈希表
准备两个哈希表hash1和hash2,分别给s和t准备
利用count记录有效字符的种类
滑动窗口:
1、初始化hash1、hash2,left、right为0
2、进窗口:当hash2[in]与hash1[in]相等时,count++
3、进窗口:当hash2[out]与hash1[out]相等时,count--
4、更新结果:起始位置,最短长度
cpp
class Solution {
public:
string minWindow(string s, string t)
{
//容器相比于数组模拟开销比较大
int hash1[128]={0}; //统计字符串t中每一个字符的频次
int kinds=0;//统计有多少种有效字符
for(auto& e:t)
{
if(hash1[e]==0) kinds++;
hash1[e]++;
}
int hash2[128]={0};//统计窗口内每一个字符的频次
int minlen=INT_MAX,begin=-1;//最短长度
for(int left=0,right=0,count=0;right<s.size();right++)
{
char in=s[right];
hash2[in]++;
if(hash2[in]==hash1[in]) count++;//进窗口+维护count
//判断
while(kinds==count)
{
//更新结果
if(right-left+1<minlen)
{
minlen=right-left+1;
begin=left;
}
//
char out=s[left++];
if(hash2[out]==hash1[out]) count--;
hash2[out]--;
}
}
if(begin==-1) return "";
else return s.substr(begin,minlen);
}
};
本期滑动窗口的内容就此结束。喜欢请点个赞支持一下,谢谢