文章目录
- 什么是滑动窗口
- 1.长度最小的子数组
- 2.无重复字符的最长子串
- 3.最大连续1的个数
- 4.将x减到0的最小操作数
- 5.最小覆盖子串
- 总结
什么是滑动窗口
滑动窗口(Sliding Window)是一种在计算机科学中用于解决各种子数组或子字符串问题的技术。滑动窗口技术通过维护一个固定大小的窗口在数组或字符串上移动,从而使得可以在较短的时间内解决一些复杂的问题。这种方法在处理一系列数据时特别高效。滑动窗口(Sliding Window)是一种在计算机科学中用于解决各种子数组或子字符串问题的技术。滑动窗口技术通过维护一个固定大小的窗口在数组或字符串上移动,从而使得可以在较短的时间内解决一些复杂的问题。这种方法在处理一系列数据时特别高效。
滑动窗口其实可以理解为一个左指针和一个右指针共同维护的一个区间,然后一起移动,这个区间的长度可能是不变的,也可能是变化的。
滑动窗口的几个基本步骤:
1.进窗口
2.判断是否出窗口
3.更新结果
接下来我们用几道题来演示滑动窗口这个算法。
1.长度最小的子数组
题目链接
题目:
样例输入和输出:
这道题的意思就是让我们求数组中和为target或者大于target的长度最小的子数组。首先我们来看看示例1:
从下图可以直观的看见最后一个子数组是最短的,所以这里输出是2,理解了题意接下来我们来想想怎么解。
解法一:暴力解法
暴力解法很容易想到,我们用两层循环将所有情况全部遍历一遍,然后用一个长度len来记录每次循环的最小的结果,然后最后遍历完了之后返回结果。
解法二:滑动窗口
暴力枚举的优化算法
我们看1,当我们遍历到1的情况的时候,按照暴力解法,先left指针后移,然后将right指针移到left指针重新遍历一遍即可,但是我们想一想有没有必要将right指针移到left的位置重新遍历,首先left指针向后移动一个位置之后只会出现两种情况,第一种情况:满足条件大于等于target,大于等于target而且长度还比前一个长度短,所以这里可以直接更新结果,没必要将right移到left重新遍历,来看第二种情况,,就是left向后移动以后不满足条件,不满足条件了,那将right移动到left重新遍历到未移动的位置都不满足,所以这里更不用重新遍历,所以综上两种情况,都不需要将right移到left的位置重新遍历。所以这里我们满足情况只需要将left++,right不需要移到left重新遍历,每次left移动之后还是要判断一下是否满足条件,满足条件则更新结果。
所以这里很快就出来了:
步骤就是1.进窗口 2.判断是否出窗口,更新结果
代码展示:
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int left = 0,right=0;
int sum=0,len=INT_MAX;//len取最大,因为len最后求的是min
while (right < nums.size())
{
//进窗口
sum+=nums[right];
//循环判断
while(sum>=target)
{
len=min(len,right-left+1);//更新结果
sum-=nums[left++];//出窗口
}
right++;
}
return len==INT_MAX?0:len;
}
};
2.无重复字符的最长子串
题目链接
题目:
样例输入和输出:
这道题题目意思很简单,,就是让我们求无重复字符的子串,并且还是最长的。
解法一:
暴力解法,暴力解法很简单,把这个字符串给遍历一遍,然后将所有的子串的情况全部求出来,每次求出一种判断一下,这里判断我们就用hash表,看看子串中字符的种类,如果每个种类只有一种说明这个子串是符合要求的,如果hash表中有一个字符是有多个的,说明这个子串是不满足的。最后返回满足的最长的子串即可。
解法二:滑动窗口+hash
首先我们还是需要用到hash表,用来记录区间内的字符的种类,但是暴力解法是逐个遍历,优化的滑动窗口当我们遍历到第一种情况的时候right指针继续向后移动。
这里这个滑动窗口已经不满足条件了所以这里我们应该让left++,left++之后right还有没有必要向前移动,很显然是没必要的,因为和上一道题相似,这里无非就是两种情况:
1.left移动之后这个区间不成立,这个区间都不成立了right还从前往后遍历是不是多余?
2.left移动之后这个区间成立了,这个区间成立,但是和前一个区间是相同长度的区间,所以向前遍历的长度都没有这个区间长,所以更不需要向前遍历。
综上right只需要向后移动,不需要向前移动。
细节:每次我们入窗口的时候都需要判断一下hash表中的字符种类的个数是否只有一个,如果有多个的话则移动left然后将hash表中的元素进行–。直到减到1个为止。然后再更新结果。
代码展示:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//创建一个hash数组用来记录重复字母的个数
int hash[128]={0};
int left=0,right=0;
int len=0;
int n=s.size();
while(right<n)
{
hash[s[right]]++;
while(hash[s[right]]>1)
{
hash[s[left]]--;
left++;
}
len=max(len,right-left+1);
right++;
}
return len;
}
};
3.最大连续1的个数
题目链接
题目:
样例输入和输出:
这道题大致的意思就是我们需要找到连续的1的个数最大的子数组,并且我们可以将0翻转成1。
对于第一个例子来说,由于我们可以将0翻转成1,所以第一个区间是最长的,第二个区间和第三个区间是一样长的,但是比第一个区间长,所以我们可以返回最长的区间长度。
解法一:暴力解法
暴力解法也只需要暴力枚举,枚举出所有的情况,但是我们这道题需要正难则反,因为如果我们真的翻转每一个零的话,这道题是很难的,这道题要我们求最长的连续的1的区间,我们可以转换为求最长的连续1的区间并且如果这个区间涵盖0的话,零的个数不能超过k个,如果这个区间的0的个数不超过k个那么这个区间就是合法的。转换了之后就很好做了,没枚举一个把这个数放进hash表,然后判断其零的个数,如果零的个数是合法的,那么这个区间就成立,则更新最长结果。
解法二:滑动窗口+hash表
这道题转换了之后就和上一道一个样了,我们只需要用一个两个空间的hash表来记录这里面1和0的个数即可,每次入完窗口之后,都判断一下这个窗口是否合法,也就是判断一下这个窗口内的0的个数是否大于k个了,如果没有大于k个,则继续,更新结果。
代码展示:
class Solution {
public:
int longestOnes(vector<int>& nums, int k)
{
//定义左右指针
int left = 0, right = 0;
//定义长度并且定义长度
int len = 0, n = nums.size();
//定义1的个数和0的个数
int arr[2]={0};
while (right < n)
{
//入窗口
arr[nums[right]]++;
//判断窗口内的零的个数,如果零大于k的时候就left++,并且出窗口
while(arr[0]>k)
{
arr[nums[left]]--;
left++;
}
//判断完了之后更新长度
len = max(len, right - left + 1);
//更新right
right++;
}
//返回最大长度
return len;
}
};
4.将x减到0的最小操作数
题目链接
题目:
样例输入和输出:
这道题的大致意思我们每次只能选最左边或者最右边的数,然后每次选了这个数之后,这个数就被删除了,不能选这个数,最后选了数我们要选出最少的数使得能将x减为零的长度。大致就是这个意思。
这道题要是我们直接做的话,会相当难,我们可以换个角度来看这个道题,这道题其实就是让我们求左右两个不连续的区间的和为x,我们用第一个例子来看:
这样转换之后这道题的难度瞬间降低了一个层次。
解法一:
暴力枚举,暴力枚举每种情况,这里我们初始化需要将len初始化为-1,如果最后len等于-1,则返回len,如果最后len不是-1,则返回的是nums的长度-len,暴力枚举每种情况,然后求出最长的那个。
解法二:
滑动窗口,这里步骤我们就不讲了,我们讲一下细节的,出窗口的条件,当我们用一个sum记录和的时候,发现这个和大于的时候就开始出窗口。
代码展示;
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum=0;
for(auto e:nums)sum+=e;
int target=sum-x;
//细节问题
if(target<0)return -1;
int ret=-1;
for(int left=0,right=0,tmp=0;right<nums.size();right++)
{
//进窗口
tmp+=nums[right];
//判断出窗口
while(tmp>target)
{
tmp-=nums[left++];
}
if(tmp==target)
{
ret=max(ret,right-left+1);
}
}
if(ret==-1)return ret;
else
return nums.size()-ret;
}
};
5.最小覆盖子串
题目链接
题目
样例输入和输出;
这道题题目意思也很简单,我们需要找到一个子串,这个子串满足在s中可以有重复的字符,但是不能少字符,意思就是s中的子串的字符的种类大于等于t中的字符种类,字符的个数也是大于等于t中的字符个数。
解法一:暴力枚举+hash表
首先我们暴力枚举出每种情况,然后再利用两个hash表,先将t中的字符存在hash表中,然后在暴力枚举的过程中,我们每入一个窗口,都需要判断其窗口内的字符种类,然后暴力枚举出满足的情况之后取子串,返回其子串。
解法二:滑动窗口+hash表
入窗口什么的还是和前面讲的一样,但是唯一有一个区别是这里是满足条件擦开始出窗口,
这里蓝色窗口的区间满足了条件,我们开始出窗口,出窗口有两种可能,就是还满足条件,还有一种是不满足条件,如果还满足条件则更新最小的结果,如果不满足了,继续入窗口,所以这里我们更新结果的地方应该在出窗口之前,这里我们需要用一个变量记录有效字符的个数,首先我们先用一个变量kinds来记录t中的字符的种类,然后再用count记录区间中有效字符的个数,在记录窗口中有效字符的个数的时候,我们只需要判断一下这个字符是否存在于t中即可,如果存在于t中则为有效字符,并且这里如果t中a有1个,s中a有很多个,这里我们只记录一次有效数据。
代码展示:
class Solution {
public:
string minWindow(string s, string t)
{
int hash1[128] = { 0 }, hash2[128] = { 0 };
int left = 0, right = 0;
int kinds = 0;//统计有效字符的个数
for (auto e : t)
{
if (hash1[e] == 0)kinds++;//统计表中的字符
hash1[e]++;
}
int count = 0;
int minlen = INT_MAX;int begin = -1;
while (right < s.size())
{
hash2[s[right]]++;
if (hash2[s[right]] == hash1[s[right]])
{
count++;
}
while (count == kinds)
{
if (right - left + 1 < minlen)
{
minlen = right - left + 1;
begin = left;
}
if (hash1[s[left]] == hash2[s[left]])
{
count--;
}
hash2[s[left]]--;
left++;
}
right++;
}
if (begin == -1)return "";
else return s.substr(begin, minlen);
}
};
总结
滑动窗口算法是一种高效且灵活的技术,它可以显著减少解决某些问题的时间复杂度。在处理数组和字符串相关的问题时,滑动窗口尤其有效,它通过动态调整窗口的大小来满足特定的条件,避免了不必要的重复计算。
在本文中,我们详细讨论了滑动窗口的基本概念和应用场景,并通过具体的例子展示了如何使用滑动窗口解决无重复字符的最长子串问题。通过这些示例,我们可以看到滑动窗口的强大之处,以及在实际编程中如何灵活应用这项技术。
希望这篇文章能帮助你更好地理解滑动窗口算法,并在以后的算法学习和实践中熟练应用这项技术。如果你有任何疑问或新的想法,欢迎在评论区留言,我们一起讨论交流。感谢阅读!