目录
1.滑动窗口算法介绍
1.1 什么是滑动窗口?
1.2 什么时候用滑动窗口?
题目:长度最小的子数组
解法:
总结:
1.3 怎么用滑动窗口?
1.4 滑动窗口的正确性
1.5 滑动窗口的时间复杂度
2.相关OJ题
2.1 长度最小的子数组
2.1.1 题目
2.1.2 算法
2.1.3 代码
2.2 无重复字符的最长子串
2.2.1 题目
2.2.2 算法
2.2.3 代码
2.3 最大连续1的个数
2.3.1 题目
2.3.2 算法
2.3.3 代码
2.4 把x减为0的最小操作数
2.4.1 题目
2.4.2 算法
2.4.3 代码
2.5 水果成篮
2.5.1 题目
2.5.2 算法
2.5.3 代码
2.6 找出字符串中所有的字母异位词
2.6.1 题目
2.6.2 算法
2.6.3 代码
2.7 串联所有单词的子串
2.7.1 题目
2.7.2 算法
2.7.3 代码
2.8 最小覆盖子串
2.8.1 题目
2.8.2 算法
2.8.3 代码
3.总结:
滑动窗口算法是一种常用的双指针算法,主要用于解决字符串和数组相关的问题。它的基本思想是通过维护一个窗口,滑动窗口的起始位置和结束位置以解决问题。
滑动窗口算法的步骤如下:
初始化左指针和右指针,分别指向窗口的起始位置。
当满足问题的条件时,右指针向右移动,扩大窗口范围;当不满足问题的条件时,左指针向右移动,缩小窗口范围。
在每次移动指针之后,判断当前窗口是否满足问题的条件,如果满足,则根据需要更新结果;如果不满足,则继续移动指针。
重复步骤2和步骤3,直到右指针到达数组或字符串的末尾。
滑动窗口算法的优点是在某些情况下,可以将时间复杂度从暴力解法的O(n^2)降低到O(n)。它适用于解决一些查找最长子串、最小覆盖子串、最小窗口子串等问题。
需要注意的是,在使用滑动窗口算法时,需要定义好窗口的起始位置和结束位置,以及对窗口进行移动和更新的条件。同时,需要考虑边界条件和特殊情况,确保算法的正确性。
滑动窗口的双指针是同向双指针,两个指针之间的数据可以想象成一个窗口,伴随着这两个指针的移动,这个窗口是在不停向前移动变化的,所以叫滑动窗口。
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于target
的长度最小的 连续子数组 ,并返回其长度。如果不存在符合条件的子数组,返回 0
。
示例 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
解法一:暴力枚举出每一个子数组,从这些子数组中找出长度最小并且和>=target的,返回其长度。O(N^2)
for(int left = 0; left < n; left++)
for(int right = left; right < n; right++)
{
....
}
解法二:滑动窗口
其实在暴力解法中有很多不必要的枚举,比如:
枚举到一个子数组的sum(子数组的元素总和)>=target时,没有必要向后枚举,因为题目要找最小子数组,后面枚举的子数组sum必定>=target,必然不是后面枚举的子数组。
枚举到一个符合条件的子数组后,left++,right就没有必要退回到left的位置,因为right回退必然导致sum
我们发现,随着right的向后枚举,其sum值是在递增的,也就是sum值是具有单调性的,我们就可以利用这个单调性来进行优化,从而省去很多不必要的枚举,也就是使用滑动窗口算法。
在整个过程中,right指针的走向:
sum符合条件,即sum >= target,right指针不动。
sum不符合条件,right指针右移。
整个过程中right指针没有回退,这也是滑动窗口算法的优势所在。
从上面这道题来看:
1.设置两个同向指针left、right;
2.进窗口(right指针后移,想象成后面元素进窗口):sum += nums[right];
3.
3.1 判断(视题而定,这里判断的是sum>=target?),根据判断结果决定是否要出窗口(left指针后移),不出则跳过3 3.2 更新结果(视题而定,这里更新子数组长度len), 3.3 出窗口 3.4 重复3.1~3.34.重复2~3
注意:
绝大多数的滑动窗口题型的模板都是固定的,但是细节处理上具体题目具体分析。
更新结果的位置不固定,这个要视题目而定。
单调性保证了他的正确性。
left指针和right指针都最多向后移动n步,复杂度O(N)
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于target
的长度最小的 连续子数组 ,并返回其长度。如果不存在符合条件的子数组,返回 0
。
示例 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
1.设置两个同向指针left、right;
2.进窗口:sum += nums[right];
3.
3.1 判断sum>=target?。如果sum >= target,则更新结果并出窗口;如果sum < target,则跳过3。 3.2 更新结果(更新子数组长度len), 3.3 出窗口 3.4 重复3.1~3.34.重复2~3
class Solution {
public:
int minSubArrayLen(int target, vector& nums)
{
int n = nums.size(), minLen = INT_MAX, sum = 0;
int left = 0, right = 0;
for(right = 0; right < n; right++)
{
//进窗口
sum += nums[right];
//判断
while(sum >= target)
{
minLen = min(minLen, right - left + 1);//更新结果
sum -= nums[left++];//出窗口
}
}
return minLen == INT_MAX ? 0 : minLen;
}
};
注意:数组中有不符合条件的子数组,也就是整个数组的sum 给定一个字符串 示例 1: 示例 2: 示例 3: 暴力解法:枚举出所有子串,找出符合条件的。 滑动窗口解法: 题目要求子串不能有重复字符,这里我们可以设置一个哈希表,指针每指向一个元素判断该元素在不在哈希表中,如果该元素不在哈希表中,则放入哈希表;如果已经在哈希表中,则重复字符就能在哈希表中快速查找到。(哈希表的查找效率为O(1)) 1.设置两个同向指针left、right; 2.进窗口:nums[right]放入哈希表; 3. 4.更新结果(子串长度len) 5.重复2~4 给定一个二进制数组 示例 1: 示例 2: 这题我们没有必要真的翻转0,可以转化成子数组问题:找出该数组中0的个数不超过k的最长连续子数组。 那么子数组问题我们又可以使用滑动窗口算法了。 1.设置两个同向指针left、right; 2.进窗口:right++并统计0的个数; 3. 4.更新结果(子串长度len) 5.重复2~4 给你一个整数数组 如果可以将 示例 1: 示例 2: 示例 3: 这种题也需要经过转化变为子数组问题: x减去数组两端元素直到为0,那么这两端移除的元素和为x; 并且每一端元素都是连续的,那么剩余的中间连续元素的和就是sum-x(设为target)(sum是整个数组的和)。 操作数最少,移除元素也就最少,中间剩余元素也就最多。 可以转化成最长子数组问题:找出该数组中和为sum-x(target)的最长连续子数组。 1.设置两个同向指针left、right; 2.进窗口:add += nums[right];(add为子数组所有元素之和) 3. 4.更新结果(如果add == target,更新子串长度len) 5.重复2~4 你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果: 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。 给你一个整数数组 示例 1: 示例 2: 示例 3: 示例 4: 根据题目理解,这题也可以转化成最长子数组的问题: 不能跳着摘水果,说明是连续子数组。 两个篮子,每个篮子只能装一种水果,说明你只能装两种水果,也就是连续子数组中只能有两种水果。 返回可以收集的水果的最大数目,也就是数组中水果种类不超过两种的最长连续子数组问题。 还有一个问题就是,水果种类数如何统计?这里用到了哈希表。 单单一个哈希表还不能解决问题,我们需要的是hashmap 算法步骤: 1.设置两个同向指针left、right; 2.进窗口:hash[nums[right]]++; 3. 4.更新结果(子数组长度len) 5.重复2~4 哈希表优化: 给定两个字符串 异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。 示例 1: 示例 2: 首先我们先解决如何判断两个str是字母异位词: 两个str排序,检查是否相同。 nlogn + n. 把两个str的字符分别放入两个哈希表中,然后检查两个哈希表是否相同。O(n) 显然方法二更优。 如何找出s中和p互成异位词的子串,这种子串问题就可以转化成滑动窗口问题: 1.开辟两个哈希表:hash1、hash2,字符串p的所有元素放入hash1中; 2.设置两个同向指针left、right; 3.进窗口:hash2[s[right]]++; 4. 5.更新结果(如果窗口长度 == len && 两个哈希表完全相同,Push(left)) 6.重复3~5 方法二:哈希表优化,不用比较两个哈希表,这种比较难想到。 类比上一题使用一个变量统计水果种类数,这里我们可以使用一个变量来统计窗口中有效字符的个数(所谓有效字符,就是能在串p中找到所对应的字符)。 如果: 如果: 注意有效字符的更新时机: s[right]是在进入哈希表后更新,s[left]是在进入哈希表前更新。 给定一个字符串 例如,如果 返回所有串联子串在 示例 1: 示例 2: 示例 3: 这题如果单独拿出来做的话,是相当难的。但是有前面题目的铺垫就相对简单了一些。 如果我们把words中的每一个字符串都当做一个字符,那这里的联串子串是不是类似上一题的字母异位词? 那我们就可以类比上一题,使用滑动窗口算法+哈希表来解决这里的问题,但是相比上一题有很多细节上的不同: 哈希表的不同:这里的哈希表我们要放字符串,所以使用unordered_map left、right指针的移动长度不同:题目中其实有个关键信息: 滑动窗口的执行次数不同:不能忘记下面两种位置的考虑,否则可能出现遗漏。 同样两种,一种比较哈希表,一种使用count变量统计有效字符串个数,不用比较哈希表。 给你一个字符串 注意: 对于 如果 示例 1: 示例 2: 示例 3: 方法一:暴力枚举+哈希表:枚举出s的每一个子串,判断是否符合条件。 方法二:滑动窗口+哈希表 1.为什么能使用滑动窗口算法? 暴力枚举的过程中也有很多不必要的枚举: right指针走到符合条件(子串覆盖t)的时候,right指针没有必要后移。 left++后,right指针不能回退,否则不符合条件。 结合以上量两点,left指针和right指针都是单向前进,我们可以使用滑动窗口算法进行优化。 2.哈希表的优化 和前面几题相同,这里我们可以对哈希表进行优化,使用一个变量来记录有效字符个数。 1.开辟两个哈希表:hash1、hash2,字符串t的所有元素放入hash1中; 2.设置两个同向指针left、right; 3.进窗口:hash2[s[right]]++并统计有效字符; 4. 5.重复3~4 题目中有涉及连续子数组或者子串问题的,都可以尝试使用滑动窗口来解决,但是有些子数组或者子串问题不易察觉,需要我们经过一定的分析才能转换成子数组、子串问题,比如:2.3、2.4、2.5、2.7。 有些问题我们需要使用哈希表来解决,但这个哈希表不一定是unordered_map/set,有时可以使用一个数组来解决问题。并且对于两个哈希表的比较,我们也可以优化成对有效数据的统计问题。 滑动窗口解题模板中的更新结果位置不固定,但一般找最长子数组、子串问题是在while循环(判断、出窗口的循环)的外面,找最短子数组、子串问题是在while循环(判断、出窗口的循环)的里面。 滑动窗口问题的关键不在于如何使用滑动窗口,而在于为什么能使用滑动窗口,这里的分析一般是根据暴力解法总结来的,子数组/子串问题的暴力解法一般很容易想出来,根据这个暴力解法我们可以分析能否使用滑动窗口算法。 2.2 无重复字符的最长子串
2.2.1 题目
s
,请你找出其中不含有重复字符的 最长子串 的长度。输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
2.2.2 算法
3.1 判断nums[right]是否在哈希表中。如果在哈希表中,则出窗口;如果不在,则跳过3。
3.2 出窗口,a[left]移出哈希表;
3.3 重复3.1~3.2
2.2.3 代码
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
int hash[128] = {0};
int left = 0, right = 0, n = s.size();
int maxLen = 0;
for(; right < n; right++)
{
//进窗口
hash[s[right]]++;
//判断
while(hash[s[right]] > 1)
{
hash[s[left++]]--;//出窗口
}
//更新结果
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
};
2.3 最大连续1的个数
2.3.1 题目
nums
和一个整数 k
,如果可以翻转最多 k
个 0
,则返回 数组中连续 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。
输入: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。
2.3.2 算法
3.1 判断窗口中0的个数是否大于k。如果大于k,则出窗口;如果不大于k,则跳过3。
3.2 出窗口,left++并统计0的个数;
3.3 重复3.1~3.2
2.3.3 代码
class Solution {
public:
int longestOnes(vector
2.4 把x减为0的最小操作数
2.4.1 题目
nums
和一个整数 x
。每一次操作时,你应当移除数组 nums
最左边或最右边的元素,然后从 x
中减去该元素的值。请注意,需要 修改 数组以供接下来的操作使用。x
恰好 减到 0
,返回 最小操作数;否则,返回 -1
。输入:nums = [1,1,4,2,3], x = 5
输出:2
解释:最佳解决方案是移除后两个元素,将 x 减到 0 。
输入:nums = [5,6,7,8,9], x = 4
输出:-1
输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
2.4.2 算法
3.1 判断add > target?。如果大于target,则出窗口;如果不大于target,则跳过3。
3.2 出窗口,add -= nums[left];
3.3 重复3.1~3.2
2.4.3 代码
class Solution {
public:
int minOperations(vector
2.5 水果成篮
2.5.1 题目
fruits
表示,其中 fruits[i]
是第 i
棵树上的水果 种类 。
fruits
,返回你可以收集的水果的 最大 数目。输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。
2.5.2 算法
3.1 判断窗口中水果种类数(hash.size())是否超过了两种。如果超过,则出窗口;如果没超过,则跳过3。
3.2 出窗口,hash[nums[left]]--,如果减到0,则从哈希表移除该元素;
3.3 重复3.1~3.2
2.5.3 代码
class Solution {
public:
int totalFruit(vector
//哈希表优化
class Solution {
public:
int totalFruit(vector
2.6 找出字符串中所有的字母异位词
2.6.1 题目
s
和 p
,找到 s
中所有 p
的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
2.6.2 算法
4.1 判断窗口长度是否大于串p的长度len。如果超过,则出窗口;如果没超过,则跳过3。
4.2 出窗口,hash2[s[left]]--;
2.6.3 代码
class Solution {
public:
bool checkHash(int* a, int* b, int n)
{
for(int i = 0; i < 26; i++)
{
if(a[i] != b[i])
return false;
}
return true;
}
vector
hash1[s[right]] <= hash2[s[right]]
,s[right]就是有效字符,count(记录有效字符)++。hash1[s[right]] > hash2[s[right]]
,s[right]就不是有效字符。//小优化,不用比较两个数组
class Solution {
public:
vector
2.7 串联所有单词的子串
2.7.1 题目
s
和一个字符串数组 words
。words
中所有字符串 长度相同。s
中的 串联子串 是指一个包含 words
中所有字符串以任意顺序排列连接起来的子串。
words = ["ab","cd","ef"]
, 那么 "abcdef"
, "abefcd"
,"cdabef"
, "cdefab"
,"efabcd"
, 和 "efcdab"
都是串联子串。 "acdbef"
不是串联子串,因为他不是任何 words
排列的连接。s
中的开始索引。你可以以 任意顺序 返回答案。输入: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] 也是可以的。
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。
s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。
所以我们返回一个空数组。
输入: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"] 顺序排列的连接。
2.7.2 算法
words
中所有字符串 长度相同。这也决定了我们能按照上一题的思路来解决本题,left和right指针每次都要走len(words中每个字符串的长度)步。2.7.3 代码
class Solution {
public:
vector
class Solution {
public:
vector
2.8 最小覆盖子串
2.8.1 题目
s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。
t
中重复字符,我们寻找的子字符串中该字符数量必须不少于 t
中该字符数量。s
中存在这样的子串,我们保证它是唯一的答案。输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。
输入:s = "a", t = "a"
输出:"a"
解释:整个字符串 s 是最小覆盖子串。
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。
2.8.2 算法
4.1 判断子串是否覆盖t(即有效字符数 == t.size()),如果覆盖则更新结果并出窗口;如果没有覆盖,则跳过4。
4.2 更新结果(窗口内子串起始位置及长度),
4.3 出窗口
4.4 重复4.1~4.3
2.8.3 代码
class Solution {
public:
string minWindow(string s, string t)
{
int len1 = s.size(), len2 = t.size();
int hash1[58] = {0}, hash2[58] = {0};
int count = 0;//有效字符个数
int begin = 0, minLen = INT_MAX;//返回的字符串的起始位置和长度
//t字符串信息读入哈希表
for(auto e : t)
{
hash1[e - 'A']++;
}
int left = 0, right = 0;
for(; right < len1; right++)
{
//进窗口
hash2[s[right] - 'A']++;
if(hash2[s[right] - 'A'] <= hash1[s[right] - 'A'])
count++;
//判断
while(count == len2)
{
//更新结果
if(right - left + 1 < minLen)
{
begin = left;
minLen = right - left + 1;
}
//出窗口
if(hash2[s[left] - 'A'] <= hash1[s[left] - 'A'])
count--;
hash2[s[left++] - 'A']--;
}
}
return minLen == INT_MAX ? "" : s.substr(begin, minLen);
}
};
3.总结: