滑动窗口主要应用于子串问题,遇到这类问题不要犹豫,直接考虑使用滑动窗口。作为双指针类问题最难掌握的技巧,其设计思路其实非常简单,就是通过两个指针维护一个窗口,通过前后指针的不断向前滑动,然后更新答案。该算法的大致逻辑如下:
1 | left := 0 |
这个算法技巧的时间复杂度是 O(N),比字符串暴力算法要高效得多。其实困扰大家的,不是算法的思路,而是各种细节问题。比如说如何向窗口中添加新元素,如何缩小窗口,在窗口滑动的哪个阶段更新结果。即便你明白了这些细节,也容易出 bug,找 bug 还不知道怎么找,真的挺让人心烦的。
算法框架
滑动窗口算法代码框架
废话少说,直接上代码。下面的框架很适合给出了具体t,要求找出其在主串s中的排列或包含t的子串的问题,可以直接套用。
1 | func slidingwindow(s string, t string) { // s是主串,t是子串 |
示例
最小覆盖子串
就是说要在 S
(source) 中找到包含 T
(target) 中全部字母的一个子串,且这个子串一定是所有可能子串中最短的。
如果我们使用暴力解法,代码大概是这样的:
1 | for i:=0; i<len(s); i++ { |
思路很直接,但是显然,这个算法的复杂度肯定大于 O(N^2) 了。
滑动窗口算法的思路是这样:
1、我们在字符串 S
中使用双指针中的左右指针技巧,初始化 left = right = 0
,把索引左闭右开区间 [left, right)
称为一个「窗口」。
2、我们先不断地增加 right
指针扩大窗口 [left, right)
,直到窗口中的字符串符合要求(包含了 T
中的所有字符)。
3、此时,我们停止增加 right
,转而不断增加 left
指针缩小窗口 [left, right)
,直到窗口中的字符串不再符合要求(不包含 T
中的所有字符了)。同时,每次增加 left
,我们都要更新一轮结果。
4、重复第 2 和第 3 步,直到 right
到达字符串 S
的尽头。
这个思路其实也不难,第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
下面画图理解一下,needs
和 window
相当于计数器,分别记录 T
中字符出现次数和「窗口」中的相应字符的出现次数。
初始状态:
增加 right
,直到窗口 [left, right]
包含了 T
中所有字符:
现在开始增加 left
,缩小窗口 [left, right]
。
直到窗口中的字符串不再符合要求,left
不再继续移动。
之后重复上述过程,先移动 right
,再移动 left
…… 直到 right
指针到达字符串 S
的末端,算法结束。
现在开始套模板,只需要思考以下四个问题:
1、当移动 right
扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动 left
缩小窗口?
3、当移动 left
缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加 window
计数器;如果一个字符将移出窗口的时候,应该减少 window
计数器;当 valid
满足 need
时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
1 | func minWindow(s string, t string) string { |
字符串的排列
给定两个字符串 s1
和 s2
,写一个函数来判断 s2
是否包含 s1
的排列。
换句话说,第一个字符串的排列之一是第二个字符串的 子串 。
示例 1:
输入: s1 = “ab” s2 = “eidbaooo”
输出: True
解释: s2 包含 s1 的排列之一 (“ba”).
示例 2:
输入: s1= “ab” s2 = “eidboaoo”
输出: False
提示:
- 输入的字符串只包含小写字母
- 两个字符串的长度都在
[1, 10,000]
之间
这种题目,是明显的滑动窗口算法,相当给你一个 S1
和一个 **S2
**,请问你 S2
中是否存在一个子串,包含 S1
中所有字符且不包含其他字符?
1 | func checkInclusion(s1 string, s2 string) bool { |
对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:
1、本题移动 left
缩小窗口的时机是窗口大小大于 S1.size()
时,应为排列嘛,显然长度应该是一样的。
2、当发现 valid == len(need)
时,就说明窗口中就是一个合法的排列,所以立即返回 true
。
至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。
找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。
字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。
说明:
字母异位词指字母相同,但排列不同的字符串。
不考虑答案输出的顺序。
示例 1:
1 | 输入: |
示例 2:
1 | 输入: |
和上一个示例几乎一模一样,只不过函数返回的东西不同。
1 | func findAnagrams(s string, p string) []int { |
无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
1 | 输入: s = "abcabcbb" |
示例 2:
1 | 输入: s = "bbbbb" |
示例 3:
1 | 输入: s = "pwwkew" |
示例 4:
1 | 输入: s = "" |
提示:
0 <= s.length <= 5 * 104
s
由英文字母、数字、符号和空格组成
这个题终于有了点新意,不是一套框架就出答案,不过反而更简单了,稍微改一改框架就行了:
1 | func lengthOfLongestSubstring(s string) int { |
- Post title:滑动窗口(Sliding Window)算法框架
- Post author:洪笳淏
- Create time:2021-04-28 14:47:30
- Post link:https://jiahaohong1997.github.io/2021/04/28/滑动窗口(Sliding-Window)算法框架/
- Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.