滑动窗口算法解题套路框架
Yuxuan Wu Lv13

介绍

  1. 滑动窗口算法可以将嵌套的循环问题,转换为单循环问题,降低时间复杂度。(循环嵌套-> 单循环问题)

img

算法的大致逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
int left = 0, right = 0;

while (right < s.size()) {
// 增大窗口
window.add(s[right]);
right++;

while (window needs shrink) {
// 缩小窗口
window.remove(s[left]);
left++;
}
}
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
/* 滑动窗口算法框架 */
void slidingWindow(string s, string t) {
unordered_map<char, int> need, window;
for (char c : t) need[c]++;

int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// c 是将移入窗口的字符
char c = s[right];
// 右移窗口
right++;
// 进行窗口内数据的一系列更新
...

/*** debug 输出的位置 ***/
printf("window: [%d, %d)\n", left, right);
/********************/

// 判断左侧窗口是否要收缩
while (window needs shrink) {
// d 是将移出窗口的字符
char d = s[left];
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
...
}
}
}

其中两处 ... 表示的更新窗口数据的地方,到时候你直接往里面填就行了

而且,这两个 ... 处的操作分别是右移和左移窗口更新操作,等会你会发现它们操作是完全对称的。

言归正传,下面就直接上四道 LeetCode 原题来套这个框架,其中第一道题会详细说明其原理,后面四道就直接闭眼睛秒杀了。

因为滑动窗口很多时候都是在处理字符串相关的问题,Java 处理字符串不方便,所以本文代码为 C++ 实现。不会用到什么编程方面的奇技淫巧,但是还是简单介绍一下一些用到的数据结构,以免有的读者因为语言的细节问题阻碍对算法思想的理解:

unordered_map 就是哈希表(字典),它的一个方法 count(key) 相当于 Java 的 containsKey(key) 可以判断键 key 是否存在。

可以使用方括号访问键对应的值 map[key]。需要注意的是,如果该 key 不存在,C++ 会自动创建这个 key,并把 map[key] 赋值为 0。

所以代码中多次出现的 map[key]++ 相当于 Java 的 map.put(key, map.getOrDefault(key, 0) + 1)

滑动窗口算法的思路是这样

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 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。

下面画图理解一下,needswindow 相当于计数器,分别记录 T 中字符出现次数和「窗口」中的相应字符的出现次数。

初始状态:

img

增加 right,直到窗口 [left, right] 包含了 T 中所有字符:

img

现在开始增加 left,缩小窗口 [left, right]

img

直到窗口中的字符串不再符合要求,left 不再继续移动。

img

之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。

如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。现在我们来看看这个滑动窗口代码框架怎么用

首先,初始化 windowneed 两个哈希表,记录窗口中的字符和需要凑齐的字符:

之后重复上述过程,先移动 right,再移动 left…… 直到 right 指针到达字符串 S 的末端,算法结束。

如果你能够理解上述过程,恭喜,你已经完全掌握了滑动窗口算法思想。现在我们来看看这个滑动窗口代码框架怎么用

首先,初始化 windowneed 两个哈希表,记录窗口中的字符和需要凑齐的字符:

1
2
unordered_map<char, int> need, window;
for (char c : t) need[c]++;

然后,使用 leftright 变量初始化窗口的两端,不要忘了,区间 [left, right) 是左闭右开的,所以初始情况下窗口没有包含任何元素:

1
2
3
4
5
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// 开始滑动
}

其中 valid 变量表示窗口中满足 need 条件的字符个数,如果 validneed.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T

现在开始套模板,只需要思考以下四个问题

1、当移动 right 扩大窗口,即加入字符时,应该更新哪些数据?

2、什么条件下,窗口应该暂停扩大,开始移动 left 缩小窗口?

3、当移动 left 缩小窗口,即移出字符时,应该更新哪些数据?

4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?

如果一个字符进入窗口,应该增加 window 计数器;如果一个字符将移出窗口的时候,应该减少 window 计数器;当 valid 满足 need 时应该收缩窗口;应该在收缩窗口的时候更新最终结果。

算法实战

76. Minimum Window Substring

难度困难1308

Given two strings s and t of lengths m and n respectively, return the minimum window substring of s such that every character in t (including duplicates) is included in the window. If there is no such substring**, return the empty string "".

The testcases will be generated such that the answer is unique.

A substring is a contiguous sequence of characters within the string.

Example 1:

1
2
3
Input: s = "ADOBECODEBANC", t = "ABC"
Output: "BANC"
Explanation: The minimum window substring "BANC" includes 'A', 'B', and 'C' from string t.

Example 2:

1
2
3
Input: s = "a", t = "a"
Output: "a"
Explanation: The entire string s is the minimum window.

Example 3:

1
2
3
4
Input: s = "a", t = "aa"
Output: ""
Explanation: Both 'a's from t must be included in the window.
Since the largest window of s only has one 'a', return empty string.
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
package sliding_window;

import java.util.HashMap;

public class MinWindow {

public String minWindow(String s, String t) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();

char[] chart = t.toCharArray();
char[] chars = s.toCharArray();
for (char key : chart) {
need.put(key, need.getOrDefault(key, 0) + 1);
}

int left = 0, right = 0, valid = 0;

// 记录最小覆盖子串的起始索引和长度
int start = 0, len = Integer.MAX_VALUE;

while (right < s.length()) {
// c 是将移入窗口的字符
char c = chars[right];
// 窗口向右移动
right++;
// 进行窗口内数据的一系列更新
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).intValue() == need.get(c).intValue()) {
valid++;
}
}
while (valid == need.size()) {
// 在这里更新最小覆盖最小子串
if (right - left < len) {
start = left;
len = right - left;
}

// d 是即将移除窗口的字符
char d = chars[left];
left++;
if (need.containsKey(d)) {
if (window.get(d).intValue() == need.get(d).intValue()) {
valid--;
}
window.put(d, window.getOrDefault(d, 0) - 1);

}

}

}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}

}

我想说的是 Java 用Map记录字母出现个数的写法,
最后一个测试用例通不过时,要明白一件事。
Integer是对象啊。。。
Integer会缓存频繁使用的数值,
数值范围为-128到127,在此范围内直接返回缓存值。
超过该范围就会new 一个对象。
浪费了我两个小时,希望有这种情况的老哥注意一下。

567. Permutation in String

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise.

In other words, return true if one of s1‘s permutations is the substring of s2.

Example 1:

1
2
3
Input: s1 = "ab", s2 = "eidbaooo"
Output: true
Explanation: s2 contains one permutation of s1 ("ba").

Example 2:

1
2
Input: s1 = "ab", s2 = "eidboaoo"
Output: false
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
package sliding_window;

import java.util.HashMap;

public class CheckInclusion {
public boolean checkInclusion(String s1, String s2) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();

char[] charS1 = s1.toCharArray();
char[] charS2 = s2.toCharArray();

// 将需要的存进hashmap中去
for (char key : charS1) {
need.put(key, need.getOrDefault(key, 0) + 1);
}

int left = 0, right = 0, valid = 0;
while (right < s2.length()) {
char c = charS2[right];
right++;
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c))) {
valid++;
}
}

System.out.println("window:"+left+"->"+right);
System.out.println("window is: "+window.toString());
System.out.println("need is: "+need.toString());
System.out.println("====================================\n");


while (right - left >= s1.length()) {
if (valid == need.size()) {
return true;
}
char d = charS2[left];
left++;
if (need.containsKey(d)) {
if (window.get(d).intValue() == need.get(d).intValue()) {
valid--;
}
window.put(d, window.getOrDefault(d, 0) - 1);
}

}
}
return false;
}
}

对于这道题的解法代码,基本上和最小覆盖子串一模一样,只需要改变两个地方:

1、本题移动 left 缩小窗口的时机是窗口大小大于 t.size() 时,因为排列嘛,显然长度应该是一样的。

2、当发现 valid == need.size() 时,就说明窗口中就是一个合法的排列,所以立即返回 true

至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。

438. Find All Anagrams in a String

难度中等599

Given two strings s and p, return an array of all the start indices of p‘s anagrams in s. You may return the answer in any order.

An Anagram is a word or phrase formed by rearranging the letters of a different word or phrase, typically using all the original letters exactly once.

Example 1:

1
2
3
4
5
Input: s = "cbaebabacd", p = "abc"
Output: [0,6]
Explanation:
The substring with start index = 0 is "cba", which is an anagram of "abc".
The substring with start index = 6 is "bac", which is an anagram of "abc".

Example 2:

1
2
3
4
5
6
Input: s = "abab", p = "ab"
Output: [0,1,2]
Explanation:
The substring with start index = 0 is "ab", which is an anagram of "ab".
The substring with start index = 1 is "ba", which is an anagram of "ab".
The substring with start index = 2 is "ab", which is an anagram of "ab".

相当于,输入一个串 S**,一个串** T**,找到** S 中所有 T 的排列,返回它们的起始索引

跟寻找字符串的排列一样,只是找到一个合法异位词(排列)之后将起始索引加入 res 即可。

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
package sliding_window;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

public class FindAnagrams {
public List<Integer> findAnagrams(String s, String p) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();

char[] charNeed = p.toCharArray();
char[] charTargetString = s.toCharArray();

for (char key : charNeed) {
need.put(key, need.getOrDefault(key, 0) + 1);
}

int left = 0, right = 0, valid = 0;
List<Integer> res = new ArrayList<>();
while (right < s.length()) {
char c = charTargetString[right];
right++;
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (need.get(c).equals(window.get(c))) {
valid++;
}
}

System.out.println("======================");
System.out.println(left + "->" + right);

while (right - left >= p.length()) {
if (valid == need.size()) {
res.add(left);
}
char d = charTargetString[left];
left++;

if (need.containsKey(d)) {
if (window.get(d).equals(need.get(d))) {
valid--;
}
window.put(d, window.getOrDefault(d, 0) - 1);
}
}
}
return res;
}
}

3. Longest Substring Without Repeating Characters

Given a string s, find the length of the longest substring without repeating characters.

Example 1:

1
2
3
Input: s = "abcabcbb"
Output: 3
Explanation: The answer is "abc", with the length of 3.

Example 2:

1
2
3
Input: s = "bbbbb"
Output: 1
Explanation: The answer is "b", with the length of 1.

Example 3:

1
2
3
4
Input: s = "pwwkew"
Output: 3
Explanation: The answer is "wke", with the length of 3.
Notice that the answer must be a substring, "pwke" is a subsequence and not a substring.

Example 4:

1
2
Input: s = ""
Output: 0

这就是变简单了,连 needvalid 都不需要,而且更新窗口内数据也只需要简单的更新计数器 window 即可。

window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了嘛。

唯一需要注意的是,在哪里更新结果 res 呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?

这里和之前不一样,要在收缩窗口完成后更新 res,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。

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
package sliding_window;

import java.util.HashMap;

public class LengthOfLongestSubString {
public int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> window = new HashMap<>();

int left = 0, right = 0, res = 0;

while(right<s.length()){
char c = s.toCharArray()[right];
right++;
window.put(c,window.getOrDefault(c,0)+1);

while(window.get(c).intValue()>1){
char d = s.toCharArray()[left];
left++;
window.put(d,window.getOrDefault(d,0)-1);
}

res = Math.max(res, right-left);


}
return res;

}
}
  • Post title:滑动窗口算法解题套路框架
  • Post author:Yuxuan Wu
  • Create time:2021-08-25 09:19:23
  • Post link:yuxuanwu17.github.io2021/08/25/2021-08-25-滑动窗口算法解题套路框架/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.