
目录
滑动窗口算法是一种常用的双指针算法,主要用于解决字符串和数组相关的问题。它的基本思想是通过维护一个窗口,滑动窗口的起始位置和结束位置以解决问题。
滑动窗口算法的步骤如下:
初始化左指针和右指针,分别指向窗口的起始位置。
当满足问题的条件时,右指针向右移动,扩大窗口范围;当不满足问题的条件时,左指针向右移动,缩小窗口范围。
在每次移动指针之后,判断当前窗口是否满足问题的条件,如果满足,则根据需要更新结果;如果不满足,则继续移动指针。
重复步骤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<int>& 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 代码
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 代码
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 代码
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 代码
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 代码
hash1[s[right]] <= hash2[s[right]],s[right]就是有效字符,count(记录有效字符)++。hash1[s[right]] > hash2[s[right]],s[right]就不是有效字符。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 代码
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 代码
3.总结: