滑动窗口

滑动窗口算法代码框架

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
void slidingWindow(string s){
Map<Character,Integer> window = new HashMap();

int left = 0,right = 0;
while(right < s.length()){
// c 是将移入窗口的字符
char c = s.charAt(right);
//增大窗口
right++;
//进行窗口内数据的更新
...

/** debug 输出位置**/
/** 结束 **/

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

其中两处 ... 表示的更新窗口数据的地方,到时候写自己的逻辑。
这两个 ... 处的操作分别是扩大和缩小窗口的更新操作。

最小覆盖子串

100%宽度

滑动窗口算法的思路

  1. 我们在字符串 S 中使用双指针中的左右指针技巧,初始化 left = right = 0,把索引左闭右开区间 [left, right) 称为一个「窗口」。
  2. 我们先不断地增加 right 指针扩大窗口 [left, right),直到窗口中的字符串符合要求(包含了 T 中的所有字符)。
  3. 此时,我们停止增加 right,转而不断增加 left 指针缩小窗口 [left, right),直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同时,每次增加 left,我们都要更新一轮结果。
  4. 重复第 2 和第 3 步,直到 right 到达字符串 S 的尽头。
  • 初始状态:
    100%宽度
  • 增加 right,直到窗口 [left, right) 包含了 T 中所有字符:
    100%宽度
  • 现在开始增加 left,缩小窗口 [left, right):
    100%宽度
  • 直到窗口中的字符串不再符合要求,left 不再继续移动:
    100%宽度
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
String minWindow(String s,String t){
if (s == null || s.length() == 0 || t == null ||t.length() == 0){
return "";
}
Map<Character,Integer> window = new HashMap();
Map<Character,Integer> need = new HashMap();
for(int i=0;i<t.length();i++){
char c = t.charAt(i);
if(need.containsKey(c)){
need.put(c,need.get(c)+1);
}else{
need.put(c,1);
}
}

int left = 0,right = 0;
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start =0,len = Integer.MAX_VALUE;
while(right < s.length()){
// c 是将移入窗口的字符
char c = s.charAt(right);
// 扩大窗口
right++;
// 进行窗口内数据的一系列更新
if(need.containsKey(c)){
if(window.containsKey(c)){
window.put(c,window.get(c)+1);
}else{
window.put(c,1);
}
if(window.get(c).equals(need.get(c))){
valid++;
}
}
// 判断左侧窗口是否要收缩
while(valid == need.size()){
// 在这里更新最小覆盖子串
if(right - left < len){
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s.charAt(left);
// 缩小窗口
left++;
// 进行窗口内数据的一系列更新
if(need.containsKey(d)){
if(window.get(d).equals(need.get(d))){
valid--;
}
window.put(d,window.get(d)-1);
}
}
}
// 返回最小覆盖子串
return len == Integer.MAX_VALUE ? "" : s.substring(start,start+len);
}

字符串排列

100%宽度

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
boolean checkInclusion(String t,String s){
if (s1.length() > s2.length()) {
return false;
}
Map<Character,Integer> window = new HashMap();
Map<Character,Integer> need = new HashMap();
for(int i=0;i<s1.length();i++){
char c = s1.charAt(i);
if(need.containsKey(c)){
need.put(c,need.get(c)+1);
}else{
need.put(c,1);
}
}

int left = 0,right = 0;
int valid = 0;
while(right < s2.length()){
char c = s2.charAt(right);
right++;
// 进行窗口内数据的一系列更新
if(need.containsKey(c)){
if(window.containsKey(c)){
window.put(c,window.get(c)+1);
}else{
window.put(c,1);
}
if(window.get(c).equals(need.get(c))){
valid++;
}
}

// 判断左侧窗口是否要收缩
while(right - left == s1.length()){
// 在这里判断是否找到了合法的子串
if(valid == need.size()){
return true;
}
char d = s2.charAt(left);
left++;
// 进行窗口内数据的一系列更新
if(need.containsKey(d)){
if(window.get(d).equals(need.get(d))){
valid--;
}
if(window.containsKey(d)){
window.put(d,window.get(d)-1);
}

}
}
}
// 未找到符合条件的子串
return false;
}
  1. 移动 left 缩小窗口的时机是窗口大小大于 t.size() 时,因为排列,长度应该是一样的。
  2. valid == need.size() 时,就说明窗口中就是一个合法的排列,所以立即返回 true。
  3. 由于这道题中 [left, right) 其实维护的是一个定长的窗口,窗口大小为 t.size()。因为定长窗口每次向前滑动时只会移出一个字符,所以可以把内层的 while 改成 if,效果是一样的。

找所有字母异位词

100%宽度

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

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
List<Integer> findAnagrams(String s,String p){
if(p.length()>s.length()){
return new ArrayList();
}
Map<Character,Integer> window = new HashMap();
Map<Character,Integer> need = new HashMap();
for(int i=0;i<p.length();i++){
char c = p.charAt(i);
if(need.containsKey(c)){
need.put(c,need.get(c)+1);
}else{
need.put(c,1);
}
}
int left =0,right =0,valid =0;
List<Integer> res = new ArrayList();// 记录结果
while(right < s.length()){
char c = s.charAt(right);
right++;
// 进行窗口内数据的一系列更新
if(need.containsKey(c)){
if(window.containsKey(c)){
window.put(c,window.get(c)+1);
}else{
window.put(c,1);
}
if(window.get(c).equals(need.get(c))){
valid++;
}
}

//判断左侧窗口是否要收缩
while(right - left >= p.length()){
// 当窗口符合条件时,把起始索引加入 res
if(valid == need.size()){
res.add(left);
}
char d = s.charAt(left);
left++;
// 进行窗口内数据的一系列更新
if(need.containsKey(d)){
if(window.get(d).equals(need.get(d))){
valid--;
}
window.put(d,window.get(d)-1);
}
}
}
return res;
}

最长无重复子串

100%宽度

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
int lengthOfLongestSubstring(String s){
Map<Character,Integer> window = new HashMap();
int left = 0,right =0,res =0;
while(right < s.length()){
char c = s.charAt(right);
right++;
// 进行窗口内数据的一系列更新
if(window.containsKey(c)){
window.put(c,window.get(c)+1);
}else{
window.put(c,1);
}

// 判断左侧窗口是否要收缩
while(window.get(c) > 1){
char d = s.charAt(left);
left++;

// 进行窗口内数据的一系列更新
window.put(d,window.get(d)-1);
}

// 在这里更新答案
res = Math.max(res,right - left);
}
return res;
}

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