滑动窗口专题¶
约 4216 个字 708 行代码 4 张图片 预计阅读时间 23 分钟
介绍¶
所谓滑动窗口,即为同向双指针移动过程中形成的间隔区域,并且这两个指针在移动的过程中不会回退
对于滑动窗口的题目可以抽象为下面的步骤:
- 定义窗口两端指针
left
和right
- 进入窗口
- 判断
- 离开窗口
- 循环2、3和4步
滑动窗口类题目一般分为两种:
- 定长窗口:代表
right
和left
构成的区间最大长度在整个遍历过程中是固定的,定长窗口更新的特点就是长度大于最大长度就需要更新窗口,并且只需要更新一次就可以重新满足条件,例如下面题目中的力扣438题和力扣30题 - 不定长窗口:代表
right
和left
构成的区间长度在整个遍历过程中是不固定的,不定长窗口因为其长度不固定,所以不能通过长度作为条件更新窗口,例如下面题目中的力扣209题、力扣3题、力扣1004题和力扣904题
示例题目¶
问题描述:
Quote
给定一个含有 n
个正整数的数组和一个正整数 target
。
找出该数组中满足其总和大于等于 target
的长度最小的子数组[numsl, numsl+1, ..., numsr-1, numsr]
,并返回其长度。如果不存在符合条件的子数组,返回 0
。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 |
|
思路分析:
-
解法1:暴力解法
本题首先想到的就是暴力解法,暴力解法的思路很简单,第一层
for
循环遍历,选出区间左端点,第二层for
循环遍历,选出区间右端点,最后一次遍历,将左右区间中的值全部相加求和,标记此时子数组的长度,如此往复直到找到子数组长度最小且子数组中元素之和>=target
。根据这个暴力思路得出其时间复杂度为\(O(N^3)\)Note
之后的题目的暴力解法基本类似,所以将不再提示暴力解法,但是实际上的滑动窗口算法就是从暴力解法演变的,因为暴力解法做了一些重复性的比较
-
解法2:滑动窗口
滑动窗口的本质就是双指针算法:一个指针在前面遍历,等到满足一定的条件时再让另一个指针移动,所以现在需要思考的就是如何找到一个指针遍历的条件以及另一个指针遍历的条件。本题主要的目标是找到数组元素之和大于等于
target
并且满足数组元素最少,所以可以推出求和即为第一个指针遍历的条件,而当第一个指针和第二个指针之间的元素之和大于等于target
就是另一个指针遍历的条件,而要求数组元素最少,就是求两个指针的差值最小
Info
「如何找到一个指针遍历的条件」在滑动窗口中也被称为「进窗口」,另一个指针遍历的条件在滑动窗口中也被称为「判断+重新构建窗口」,因为「重新构建窗口」涉及到前面的元素离开原先的窗口,所以也被称为「出窗口」,后续也会以滑动窗口的术语展开
关键步骤:
以数组[2,3,1,2,4,3]
为例
单调性:因为题目给出了一个条件正整数的数组,在正整数范围内求和可以得到一个单调性的规律:加的数字越多和越大,所以如果第一次找到了一个和满足>=target
,则该下标后的数值即可不需要遍历,例如下图中的4和3即可不需要遍历
正向性:因为在上一次的遍历过程中,已经找到了一组子数组和满足>=target
,并且根据单调性可以得出right
不需要再向后移动,接下来需要更新left
寻找下一组,那么此时right
是否需要回退到left
的位置重新再来一次遍历呢?答案是不需要(如果回去就退化为了暴力解法),因为left
在移动的过程中,区间[left, right]
是开始时(left=0)[left, right]
区间的子区间,所以此时的区间(left=1)[left, right]
中的元素和即为(left=0)[left,right]区间和-left(left=1)=0时的值
一般满足以上两个性质,就可以使用滑动窗口算法,这个算啊可以将暴力解法优化到\(O(N)\),即只需要一次遍历
在本题中,滑动窗口的两端即为left
和right
,而滑动窗口中所维护的信息即为子数组之和sum
,根据滑动窗口的基本解题步骤可以得出现在需要找到何时进窗口、判断以及何时出窗口
- 何时进窗口(移动
right
扩大当前窗口):本题中,因为窗口中维护的信息是sum
,所以当开始求和时即为进窗口 - 判断:本题中窗口更新条件时
sum>=target
- 何时出窗口(移动
left
构建新窗口):本题中,根据判断条件sum>=target
可以得出,此条件成立时,证明已经得出了一个合理的结果,需要更新sum
和子数组长度len
,并让left
向后移动(移动窗口,即出窗口),这一过程被称为更新结果
需要注意,本题需要求出子数组长度的最小值,所以保存长度的结果变量len
不可以初始化为0,否则最后结果只会为0
Note
更新结果部分一般可能出现在出窗口之前或者出窗口结束之后,具体视题目而定
具体步骤如下:
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
相关题目¶
力扣3.无重复字符的最长子串¶
问题描述:
Quote
给定一个字符串s
,请你找出其中不含有重复字符的最长子串的长度。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 3 |
|
示例 3:
C++ | |
---|---|
1 2 3 4 |
|
思路分析:
本题整体思路与例题类似,唯一需要注意的就是因为要找出无重复字符,所以需要涉及到去重操作
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
力扣1004.最大连续1的个数Ⅲ¶
问题描述:
Quote
给定一个二进制数组 nums 和一个整数 k,如果可以翻转最多 k 个 0 ,则返回 数组中连续 1 的最大个数 。
示例 1:
C++ | |
---|---|
1 2 3 4 |
|
示例 2:
C++ | |
---|---|
1 2 3 4 |
|
思路分析:
- 解法1:暴力解法
- 解法2:滑动窗口 本题需要注意使用一个计数器
zero
统计0的个数而非通过一个变量依次减少直到小于k
,从而变向达到翻转0的目的来构建一个滑动窗口,因为小于k
不止一个情况,例如k=2
时,假设tmpk
是记录存储k
以便后面重新更新k
,tmpk
可以有0,1两种情况都小于k
,甚至逻辑上还有可能存在负数。但是考虑到大于k
只需要考虑一次,尽管大于k
有3,4...多种情况,但是第一次大于k
只有一种情况,所以通过计数器改变统计当前0的个数
关键步骤:
注意,题目提到最多翻转k
个0,所以可能出现翻转0,1,2,3...k
,言外之意就是翻转0的个数小于等于k
,故存在k
特别大而zero
计数器一直不可能大于k
的情况。综上,考虑在出窗口结束后更新结果
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
力扣1658.将x减到0的最小操作数¶
问题描述:
Quote
给你一个整数数组nums
和一个整数x
。每一次操作时,你应当移除数组nums
最左边或最右边的元素,然后从x
中减去该元素的值。请注意,需要修改数组以供接下来的操作使用。
如果可以将x
恰好减到0,返回最小操作数;否则,返回-1。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 |
|
示例 3:
C++ | |
---|---|
1 2 3 |
|
思路分析:
- 解法1:暴力解法
- 解法2:滑动窗口 本题使用到正难则反的策略,因为本题既要考虑左侧又要考虑右侧,但是连续的部分是中间区域,所以可以从中间区域下手构建滑动窗口
关键步骤:
本题就可以转换为「求出最长的一段区间(此时剩余区间就是最小的)其中的和大于等于数组和sumNums-x
」,需要注意本题有两个特殊情况:
- 当
sumNums-x == 0
时,此时最小的操作数的个数就是整个数组 - 当
sumNums-x < 0
时,说明无法使x减到0,自然也就无法算出sumNums-x == target
的情况
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
力扣904.水果成篮¶
问题描述:
Quote
你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组fruits
表示,其中fruits[i]
是第i
棵树上的水果种类 。
你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:
- 你只有两个篮子,并且每个篮子只能装单一类型的水果。每个篮子能够装的水果总量没有限制。
- 你可以选择任意一棵树开始采摘,你必须从每棵树(包括开始采摘的树)上恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
- 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。
给你一个整数数组fruits
,返回你可以收集的水果的最大数目。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 3 4 |
|
示例 3:
C++ | |
---|---|
1 2 3 4 |
|
示例 4:
C++ | |
---|---|
1 2 3 |
|
思路分析:
- 解法1:暴力解法
- 解法2:滑动窗口 根据题目的描述,可以联想出本题涉及到一个窗口的进入和更新:窗口进入:当前元素不存在时,插入到当前容器,窗口更新:当元素个数超过两个时,考虑更新窗口直到重新满足条件,所以可以使用滑动窗口算法
关键步骤:
本题并没有提到每一个元素只出现一次,但是需要确保容器中只有两个元素,所以需要用到可以去重的容器,可以选择的结构:红黑树,容器对应的就是set和map,也可以考虑使用unordered_map或者unordered_set。但是在更新窗口时,如果直接删除容器中的元素可能会因为每一个元素不止出现一次导致提前更新窗口,例如[3,3,3,1,2,1,1,2,3,3,4],如果直接使用一个红黑树容器,在插入时3,1,2,此时红黑树的元素个数size
为3需要更新窗口,进入循环更新逻辑时,因为没有对元素进行计数,导致set
直接删除起始指针start
指向的第一个3,现在size
重新回到2,循环提前结束.所以可以考虑使用map或者unordered_map,如果元素不存在直接插入,否则只更新计数器。最后,更新计数器不可以在更新窗口中更新,因为不能确定一定会出现3种水果
优化思路:
本题因为数据量较小,最大为10万,也可以考虑不使用库中的哈希表,而使用一个数组采用直接定址法,减少时间和空间复杂度
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
力扣438.找到字符串中所有字母异位词¶
问题描述:
Quote
给定两个字符串s
和p
,找到s
中所有p
的异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
示例 1:
C++ | |
---|---|
1 2 3 4 5 |
|
示例 2:
C++ | |
---|---|
1 2 3 4 5 6 |
|
思路分析:
- 解法1:暴力解法
-
解法2:滑动窗口
分析本题前需要先理解何为「异位词」,所谓「异位」就是不同位置,所以「字母异位词」就代表一个只有字母构成的字符串中的所有字母重新打乱,并且每个字母只能使用一次构成的新字符串。所以本题的题意中,在
s
中找p
的异位词说明s
中包含p
的异位词,而根据异位词的特点「重新打乱」可以推出构成字母异位词的新字符串中不可以有原字符串不存在的字母,所以可以推出s
中涵盖p
的每一个异位词长度等于p
字符串的长度,利用这个特点就可以使用长度为p
字符串长度的定长滑动窗口
关键步骤:
首先要考虑的就是如何判断s
中存在p
的异位词,最直观的想法就是通过两个哈希表,假设为ori
和sub
,其中ori
哈希表统计s
中的字符出现的个数,sub
中记录p
中的字符出现的个数,在启动窗口前,先统计p
中字符出现的个数,题目并没有提到p
中的字符不重复,所以通过比较个数是最合理且有效的。在比较过程中,进窗口就是将当前的字符插入到ori
哈希表中,如果ori
哈希表中和sub
哈希表中对应的字符个数相同,说明出现了一个异位词,此时就需要更新一次结果,接下来如果right
再继续向后移动导致窗口长度大于固定长度就需要出窗口,本题出窗口的本质就是让left
位置的字母的在哈希表中的个数减少1,再更新left
以s = "cbaebabacd", p = "abc"
为例,示意图如下:
优化思路:
- 容器优化:本题中的
s
和p
都只有字母,所以可以直接使用一个数组哈希表进行字母ASCII值映射 - 更新结果比较逻辑优化:本题可以考虑使用一个计数器
count
统计s
中有效字符的个数,更新逻辑分为以下三种:- 当字符进入窗口后,如果当前比较的字符存在与
ori
中,但不存在于sub
中,那么说明不是有效字符,count
不变。如果既存在于ori
,也存在于sub
中,说明是有效字符,改变count
。注意更新逻辑为小于等于而不是仅等于,因为可能存在重复字符 - 当left所在位置的字符个数更新之前需要更新计数器,分为三种逻辑:
- 当出去的字符在哈希表
ori
中的个数比哈希表sub
中的多,此时说明移出去的是多余的字符,不需要更新count
- 当出去的字符在哈希表
ori
中的个数与哈希表sub
中的相等,此时说明有效字符被移除,需要更新count
- 当出去的字符在哈希表
ori
中不存在,但是在哈希表sub
中存在,此时个数关系就是小于,说明sub
中有重复的字符,此时ori
中只出现了一次这个重复字符但是要被移除,所以依旧是有效字符被移除,需要更新count
,例如s="abacc", p="abbc"
- 当出去的字符在哈希表
- 如果有效字符个数
count
与p
的长度相同,则一定是异位词
- 当字符进入窗口后,如果当前比较的字符存在与
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
|
力扣30.串联所有单词的子串¶
问题描述:
Quote
给定一个字符串s
和一个字符串数组words
。words
中所有字符串`长度相同。
s
中的串联子串是指一个包含words
中所有字符串以任意顺序排列连接起来的子串。
例如,如果words = ["ab","cd","ef"]
, 那么"abcdef"
,"abefcd"
,"cdabef"
,"cdefab"
,"efabcd"
和"efcdab"
都是串联子串。"acdbef"
不是串联子串,因为他不是任何words
排列的连接。 返回所有串联子串在s
中的开始索引。你可以以任意顺序返回答案。
示例 1:
C++ | |
---|---|
1 2 3 4 5 6 |
|
示例 2:
C++ | |
---|---|
1 2 3 4 5 |
|
示例 3:
C++ | |
---|---|
1 2 3 4 5 6 |
|
思路分析:
- 解法1:暴力解法
- 解法2:滑动窗口 本题主体思路与上题相同,但是需要注意的是,本题比较的不是字符,而是字符串,题目提到「
words
中所有字符串长度相同」,就可以考虑使用定长滑动窗口,另外可以考虑使用整体法,因为题目提到了words
中的每一个字符的长度相同,并且words
数组的长度并不是很长,比较时取出s
中长度与words
中每一个字符串长度相同的子字符串比较,剩余的思路就和上题一样
关键步骤:
需要注意,本题并不能保证符合要求的串联子串的第一个字符在s
中的位置一定是words
每个字符串长度的整数倍,所以需要进行words
数组每一个字符串的长度次的滑动窗口,示意图如下:
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
力扣76.最小覆盖子串¶
问题描述:
Quote
给你一个字符串s
、一个字符串t
。返回s
中涵盖t
所有字符的最小子串。如果s
中不存在涵盖t
所有字符的子串,则返回空字符串""
。
注意:
对于t
中重复字符,我们寻找的子字符串中该字符数量必须不少于t
中该字符数量。 如果s
中存在这样的子串,我们保证它是唯一的答案。
示例 1:
C++ | |
---|---|
1 2 3 |
|
示例 2:
C++ | |
---|---|
1 2 3 |
|
示例 3:
C++ | |
---|---|
1 2 3 4 |
|
思路分析:
- 解法1:暴力解法 枚举出所有包含查找子串的所有字符的字符串,比较长度取出最小的子串
- 解法2:滑动窗口 根据暴力解法,每一次枚举所有包含的子串这个过程中涉及到一些重复的步骤,例如已经完全包含内容的子串被多次枚举,而如果需要将这个过程中的枚举次数减少,策略就是找到最接近最优解的一个子串,整个遍历过程中,当一个指针在遍历
s
字符串时,这个指针离起始位置的距离越来越远,此时与起始位置的指针就构成一个区间,而当刚好满足找到子串的条件时,就需要减小区间,确保能找到更小一点的区间,而这个过程就正好满足不定长滑动窗口的过程
关键步骤:
题目提到了t
字符串中的重复字符也需要完全匹配,所以需要使用哈希表来统计出现的次数
构建窗口:在t
字符串的哈希表sub
中找s
字符串中的字符,如果出现添加到另一个哈希表ori
中进行计数,方便更新窗口时比较;
更新窗口:当哈希表sub
中字符的个数与ori
中对应的字符个数相同或者ori
中的对应字符个数大于sub
中字符的个数,说明一定存在子串包含t中的所有字符,此时就需要更新窗口。否则一定不需要更新窗口。更新窗口的过程中需要记录当前子串的长度已经起始位置方便最后截取字符串,注意一定要比较新的len
和已经记录的len
,当新的len
较小时再更新起始位置,否则会出现起始位置一直被更新,包括新len
和旧len
相同的情况
更新逻辑:让ori
中的字符个数与sub
中的字符个数不匹配,即类似于从ori
中依次移除出现于t
中的字符
本题只需要考虑短的字符串中的字符即可,对于s
字符串来说,其他字符是否存在不需要考虑(因为插入会有一定时间和空间消耗)
优化思路:
- 容器优化:本题因为只含有字母,所以可以考虑使用一个数组形式的哈希表。注意因为在函数中要比较种类,所以在映射开始之前需要先判断某个字符是不是第一次出现,如果是第一次出现种类计数器就更新,否则就不更新
- 更新结果逻辑优化:与前面类似,本次优化版本主要优化更新结果逻辑,利用一个变量来统计字符种类,此处不是字符个数。统计字符种类的逻辑与字符个数不同,统计种类时只需要保证出现的字符是
t
中的字符且对应字符出现的个数等于t
中对应字符的个数就算一次种类更新,否则就不算。之所以要确保个数相等是因为t
中可能存在重复字符,出现重复字符必须保证s中也有相同数量的重复字符,如果只统计第一次出现,则不能保证s
中的重复字符数量与t
中相同,而如果是统计字符个数只需要满足是t
中的字符就更新
参考代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|