一、什么是“滑动窗口”?适用于那种题?
1.滑动窗口就是用“同向双指针”来实现的,俩个指针同时移动来模拟滑动的一个窗口。
2.滑动窗口算法通常适用于以下类型的问题:
(1)最大和最小值问题:例如,在给定大小的窗口内找到数组的最大值或最小值。
(2)固定长度的子数组问题:例如,寻找具有固定长度k的所有子数组的最大平均值。
(3)动态窗口大小的问题:例如,寻找满足某些条件的最小子数组长度。
(4)字符串模式匹配:例如,在字符串中查找某个模式的所有出现,像字母异位词、无重复字符的最长子串等问题。
(5)累积和或乘积:例如,寻找和为指定值的连续子数组的数量。
使用滑动窗口技术可以避免暴力搜索方法中不必要的重复计算,因此可以在许多情况下提高算法效率。当面对涉及连续子序列或子数组的问题时,考虑是否可以通过滑动窗口来简化解决方案是一个好习惯。
注意:滑动窗口也要进行区别,有的题型窗口大小不是固定的(left指针固定,right指针一直向右移动),有的题型窗口的大小是不变的(第六题“找到字符串中所有的异位词”中,求出p的长度,再根据p的长度来求子串中元素的数量)
二、相关题目
1.长度最小的子数组
1.题目描述
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的子数组[numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
2.示例
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组[4,3]
是该条件下的长度最小的子数组。示例 2:
输入:target = 4, nums = [1,4,4] 输出:1示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1] 输出:0
3.解题思路
这道题是寻找他的最小子数组,我们从题目中可以看出来,数组中的数字都是正整数,我们可以让右指针递增(进窗口),因此在求和的过程中由于单调性所有求和都只会增加,如果sum>=target了,记录找到的子数组长度len,并且出窗口,直到right出数组。
4.代码实现
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size(), sum = 0, len = INT_MAX;
for(int left = 0, right = 0; right < n; right++)
{
sum += nums[right];//进窗口
while(sum >= target)//判断
{
len = min(len , right - left + 1);
sum -= nums[left];//出窗口
left++;
}
}
return len == INT_MAX ? 0 : len;
}
};
2. 无重复字符的最长子串
1.题目描述
给定一个符串 s ,请你找出其中不含有重复字符的最长子串的长度。
2.示例
示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是"abc"
,所以其长度为 3。示例 2:
输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是"b"
,所以其长度为 1。示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是"wke"
,所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke"
是一个子序列,不是子串。
3.解题思路
这道题是判断最长子串的长度,因此可以使用滑动窗口的算法来解决问题。滑动窗口解决问题,主要分为四大步。第一步是进入窗口,第二步是判断,第三步是出窗口,第四步是更新数据返回值,但这四步中,判断和更新答案可能是在进入窗口前,也可能是进入窗口后。
在这道题中,使用一个数组代替hash表,使用right指针每次进入hash表(入窗口)让hash表的值++,判断的条件是,如果hash表里的值大于1(即窗口里出现了俩个一样的字符),那么进行出窗口,然后判断子串的长度len,然后让right指针继续向右走,直至遍历完字符串。
4. 代码实现
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int hash[128] = {0};
int n = s.size(), len = 0, left = 0, right = 0;
while(right < n)
{
hash[s[right]]++;//进窗口
while(hash[s[right]] > 1)//判断
{
hash[s[left++]]--;//出窗口
}
len = max(len , right - left + 1);
right++;//让下一个字符进入窗口
}
return len;
}
};
3.最大连续1的个数III
1.题目描述
给定一个二进制数组 nums
和一个整数 k
,假设最多可以翻转 k
个 0
,则返回执行操作后 数组中连续 1
的最大个数 。
2.示例
示例 1:
输入:nums = [1,1,1,0,0,0,1,1,1,1,0], K = 2 输出:6 解释:[1,1,1,0,0,1,1,1,1,1,1] 粗体数字从 0 翻转到 1,最长的子数组长度为 6。示例 2:
输入:nums = [0,0,1,1,0,0,1,1,1,0,1,1,0,0,0,1,1,1,1], K = 3 输出:10 解释:[0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1] 粗体数字从 0 翻转到 1,最长的子数组长度为 10。
3.解题思路
这道题是最多将0翻转k次,然后寻找最大连续1的子串。同样的寻找子串,我们还是使用滑动窗口的方法,首先我们明确四步,进窗口、出窗口、判断、更新数据。
我们要让right指针向右走,定义一个zero计数器,如果right指针遇到0(进入窗口),由于最多翻转k个0,所以当zero(已经翻转0的个数)大于k的时候进行出窗口操作,如果nums[left]等于0时,zero--
4.代码实现
class Solution {
public:
int longestOnes(vector<int>& nums, int k) {
int len = 0;
for(int left = 0, right = 0, zero = 0, n = nums.size(); right < n; right++ )
{
if(nums[right] == 0) zero++;
while(zero > k)
if(nums[left++] == 0) zero--;
len = max(len , right - left + 1);
}
return len;
}
};
4.将x减到0的最小操作数
1.题目描述
给你一个整数数组 nums
和一个整数 x
。每一次操作时,你应当移除数组 nums
最左边或最右边的元素,然后从 x
中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。
如果可以将 x
恰好 减到 0
,返回 最小操作数 ;否则,返回 -1
。
2.示例
示例 1:
输入:nums = [1,1,4,2,3], x = 5 输出:2 解释:最佳解决方案是移除后两个元素,将 x 减到 0 。示例 2:
输入:nums = [5,6,7,8,9], x = 4 输出:-1示例 3:
输入:nums = [3,2,20,1,1,3], x = 10 输出:5 解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
3.解题思路
根据题目我们知道,我们要在数组nums中,让数组的左边与右边相加等于x,可以进行转换,即求nums数组中,最长的值为sum(nums)的子串,这样就把这道题转换成了滑动窗口的题型了。
a.先定义一个sum来求数组中元素的和,target是目标值(target = sum - x)
b.注意:如果target的值为负数那就证明找不到结果
c.进入循环right < nums.size()时循环结束
d.进入窗口tmp += nums[right],判断出窗口:当tmp>target时,left指针向右移动tmp -= nums[left]
e.输出结果
4.代码实现
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum = 0, ret = -1;
for(int a : nums) sum += a;
int target = sum - x;
if(target < 0) return -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 -1;
else return nums.size() - ret;
}
};
5.水果成篮
1.题目描述
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits
表示,其中 fruits[i]
是第 i
棵树上的水果 种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
- 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
- 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
- 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组 fruits
,返回你可以收集的水果的 最大 数目。
2.示例
示例 1:
输入:fruits = [1,2,1] 输出:3 解释:可以采摘全部 3 棵树。示例 2:
输入:fruits = [0,1,2,2] 输出:3 解释:可以采摘 [1,2,2] 这三棵树。 如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。示例 3:
输入:fruits = [1,2,3,2,2] 输出:4 解释:可以采摘 [2,3,2,2] 这四棵树。 如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。示例 4:
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4] 输出:5 解释:可以采摘 [1,2,1,1,2] 这五棵树。
3.解题思路
根据题目要求进行转换,找fruits数组中的最长子串,要求子串中的水果类型不超过俩种。
a.创建一个哈希表来存储水果的种类
b.进入窗口,hash[fruits[right]]++;
-
right
指针向右移动,表示窗口的右边界扩展。 -
fruits[right]
是新进入窗口的水果类型。 -
hash[fruits[right]]++
表示将这种水果的计数加 1
c.判断如果种类大于2了,那么就进行出窗口hash[fruits[left]]--;
-
当窗口内的水果种类超过两种时(即
hash.size() > 2
),我们需要缩小窗口。 -
left
指针向右移动,表示窗口的左边界收缩。 -
hash[fruits[left]]--
表示将窗口左边界的水果类型的计数减 1。 -
如果某种水果的计数减到 0,说明这种水果已经不在窗口内了,因此我们从哈希表中删除这个键值对。
4.代码实现
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int , int> hash;//统计窗口内出现了多少种水果
int ret = 0;
for(int left = 0, right = 0;right < fruits.size(); right++)
{
//进窗口
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);
}
return ret;
}
};
6.找到字符串中所有的异位词
1.题目描述
给定两个字符串 s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序
2.示例
示例 1:
输入: s = "cbaebabacd", p = "abc" 输出: [0,6] 解释: 起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。 起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。示例 2:
输入: s = "abab", p = "ab" 输出: [0,1,2] 解释: 起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。 起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。 起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
3.解题思路
首先我们需要搞懂什么是异位词:字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次。
a.这道题我们根据题意然后进行转换,在字符串s中求p的异位子串,然后返回子串的起始的值。
b.为了存储p中的值,只需要将p中各个符号出现的个数统计并放在一个哈希表中
c.求p字符串的长度,然后再s中用滑动窗口的方式寻找p
d.确保固定的窗口长度中hash1与hash2中元素是否相同,我们要进行判断俩个哈希表中的值
注意:如果使用c++重载中的==来直接判断俩个哈希表是否相等,会让时间和空间的复杂度变高,因此我们需要优化
使用count用于记录当前窗口中与 p
中字符匹配的字符个数
-
窗口滑动过程:
-
进窗口:当
right
指针向右移动时,将s[right]
对应的字符加入窗口,并在hash2
中增加该字符的计数。如果该字符在hash2
中的计数不超过hash1
中的计数,则count
加 1。 -
出窗口:当窗口大小超过
p
的长度时,移动left
指针,将s[left]
对应的字符移出窗口,并在hash2
中减少该字符的计数。如果该字符在hash2
中的计数不超过hash1
中的计数,则count
减 1。 -
匹配成功:当
count
等于p
的长度时,说明当前窗口中的字符与p
中的字符完全匹配,记录下left
指针的位置。
-
-
返回结果:
-
将所有匹配的起始位置存储在
ret
中并返回。
-
注意:
hash1[ch - 'a']++;
这行代码的作用是统计字符串 p
中每个字符的出现次数。下面详细解释它的含义:
1. ch - 'a'
的含义
-
ch
是字符串p
中的一个字符。 -
'a'
是字符a
的 ASCII 值。 -
ch - 'a'
的作用是将字符ch
映射到一个 0 到 25 的索引:-
如果
ch
是'a'
,则ch - 'a'
的结果是0
。 -
如果
ch
是'b'
,则ch - 'a'
的结果是1
。 -
以此类推,
'z'
会映射到25
。
-
4.代码实现
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> ret;
int hash1[26] = { 0 };//统计p中字符出现的个数
for(auto ch : p)
hash1[ch - 'a']++;
int hash2[26] = { 0 };//统计窗口出现的个数
int m = p.size();
for(int left = 0, right = 0, count = 0; right < s.size(); right++)
{
char in = s[right];
hash2[in - 'a']++;//进窗口
if(hash2[in - 'a'] <= hash1[in - 'a']) count++;//维护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);
}
return ret;
}
};
7.串联所有单词的子串
1.题目描述
给定一个字符串 s
和一个字符串数组 words
。 words
中所有字符串 长度相同。
s
中的 串联子串 是指一个包含 words
中所有字符串以任意顺序排列连接起来的子串。
- 例如,如果
words = ["ab","cd","ef"]
, 那么"abcdef"
,"abefcd"
,"cdabef"
,"cdefab"
,"efabcd"
, 和"efcdab"
都是串联子串。"acdbef"
不是串联子串,因为他不是任何words
排列的连接。
返回所有串联子串在 s
中的开始索引。你可以以 任意顺序 返回答案。
2.示例
示例 1:
输入:s = "barfoothefoobarman", words = ["foo","bar"] 输出:[0,9]
解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。 子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。 子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。 输出顺序无关紧要。返回 [9,0] 也是可以的。示例 2:
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。 s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。 所以我们返回一个空数组。示例 3:
输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"] 输出:[6,9,12] 解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。 子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。 子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。 子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。
3.解题思路
看到这道题首先我们想到的还是滑动窗口+哈希表的方法,相较于上道题使用的哈希表来存储字母出现的个数,这道题则需要统计单词出现的次数,因此我们要使用容器进行存储(unordered_map)
1.统计words里单词出现的次数
2.这道题进出窗口是根据words中单词的大小决定的(判断条件)
3.由于words中单词的长度是3,我们把s划分为三个一组,一共出现了三组,因此发现了这个次数与words中的单词长度相同
4.将每一组都进入滑动窗口中统计
4.代码实现
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
unordered_map<string , int> hash1;//保存words里面所有单词的次数
vector<int> ret;
for(auto& s : words) hash1[s]++;
int len = words[0].size(), m = words.size();
for(int i = 0; i < len; i++)//执行len次滑动窗口
{
unordered_map<string , int> hash2;//维护窗口内单词出现的次数
for(int left = i, right = i, count = 0; right + len <= s.size(); right +=len)
{
//进窗口+维护count
string in = s.substr(right , len);
hash2[in]++;
if(hash2[in] <= hash1[in]) count++;
//判断
if(right - left + 1 >len * m)
{//出窗口+维护count
string out = s.substr(left , len);
if(hash2[out] <= hash1[out]) count--;
hash2[out]--;
left += len;
}
if(count == m) ret.push_back(left);
}
}
return ret;
}
};
8.最小覆盖子串
1.题目描述
给你一个字符串 s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
注意:
- 对于
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 - 如果
s
中存在这样的子串,我们保证它是唯一的答案。
2.示例
示例 1:
输入:s = "ADOBECODEBANC", t = "ABC" 输出:"BANC" 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。示例 2:
输入:s = "a", t = "a" 输出:"a" 解释:整个字符串 s 是最小覆盖子串。示例 3:
输入: s = "a", t = "aa" 输出: "" 解释: t 中两个字符 'a' 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。
3.解题思路
1.定义俩个哈希表hash1用来记录子串的信息,hash2用来记录目标串t的信息,kinds是t中一共出现的字符种类
2. 先将 t 的信息放⼊ 2 号哈希表中
3.使用滑动窗口进行判断
4. 进窗口 + 维护 count
char in = s[right];
hash2[in]++;
if(hash2[in] == hash1[in]) count++;
-
in
是进入窗口的字符。 -
hash2[in]++
:将in
字符的计数加 1。 -
if(hash2[in] == hash1[in]) count++
:-
如果
in
字符在窗口中的计数等于t
中该字符的计数,说明这个字符的种类已经匹配完成,因此count
加 1。
-
5. 判断窗口是否满足条件
while(count == kinds)
-
如果
count
等于kinds
,说明当前窗口中的字符已经包含了t
中的所有字符。
6. 更新最小窗口
if(right - left + 1 < minlen)
{
minlen = right - left + 1;
begin = left;
}
-
如果当前窗口的长度小于
minlen
,则更新minlen
和begin
。
7. 出窗口 + 维护 count
char out = s[left++];
if(hash2[out] == hash1[out]) count--;
hash2[out]--;
-
out
是移出窗口的字符。 -
hash2[out]--
:将out
字符的计数减 1。 -
if(hash2[out] == hash1[out]) count--
:-
如果
out
字符在窗口中的计数等于t
中该字符的计数,说明移出窗口后,这个字符的种类不再匹配,因此count
减 1。
-
8. 返回结果
if(begin == -1) return "";
else return s.substr(begin, minlen);
-
如果
begin
仍然是-1
,说明没有找到符合条件的子串,返回空字符串。 -
否则,返回从
begin
开始、长度为minlen
的子串。
9.count
的维护
-
count
表示当前窗口中与t
中字符匹配的字符种类数。 -
当
count
等于kinds
时,说明当前窗口中的字符已经包含了t
中的所有字符。
4.代码实现
class Solution {
public:
string minWindow(string s, string t) {
int hash1[128] = { 0 };//记录t中每个字符出现的频次
int kinds = 0;//统计有效字符的个数
for(auto ch : t)
if(hash1[ch]++ == 0) kinds++;
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++;
while(count == kinds)
{
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);
}
};