最长覆盖子串,异位词,最长无重复子串等等许多子串问题用常规暴力法费时费力,一些大佬的解法虽然很强效率很高,但是太难想到了,这类问题用滑动窗口算法解决非常的快捷简便。
滑动窗口算法思想
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 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
引用东哥(labuladong)的图,便于理解
首先增加right
,直到窗口[left, right)
包含了T
中所有字符:
现在开始增加left
,缩小窗口[left, right)
。
直到窗口中的字符串不再符合要求,left
不再继续移动。
之后重复上述过程,先移动right
,再移动left
…… 直到right
指针到达字符串S
的末端,算法结束。
在使用算法的时候需要考虑几个问题:
1、当移动right
扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动left
缩小窗口?
3、当移动left
缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新
滑动窗口算法框架
在Java中,通常使用HashMap来实现滑动窗口,有一个通用模板:
public static String minWindow(String s, String t) {
HashMap<Character, Integer> need = new HashMap<>();
int t_len = t.length();
for (int i = 0; i < t_len; i++) {
need.put(t.charAt(i), need.getOrDefault(t.charAt(i), 0) + 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++;
// 进行窗口内数据的一系列更新
...
// 判断左侧窗口是否要收缩
while (...) {
// d 是将移出窗口的字符
char d = s.charAt(left);
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
}
}
return ...;
}
其中getOrDefault() 方法用于获取指定key对应对value,如果找不到key,则返回设置的默认值。
如:
HashMap<Character, Integer> hashMap = new HashMap<>();
hashMap.put('b', 10);
int num1 = hashMap.getOrDefault('a', 0);
int num2 = hashMap.getOrDefault('b', 0);
System.out.println(num1);
System.out.println(num2);
key:'a'不存在,num1为默认值0,'b'存在,就使用'b'对应的value,num2为10.
案例
最小覆盖子串
记个点:
判断数组相等不能用==,要用Arrays.equals()
class Solution {
public String minWindow(String s, String t) {
// 需要包含t中的所有字符
Map<Character, Integer> need = new HashMap<>();
for(char c : t.toCharArray())
need.put(c, need.getOrDefault(c, 0) + 1);
//滑动窗口
Map<Character, Integer> window = new HashMap<>();
/*[left, right) 窗口大小:right - left; */
int left = 0, right = 0; //初始时,窗口不包含任何元素
int valid = 0; //字符找齐了,valid++;
int size = s.length();
int minLen = size + 1; //包含t的子串不止1个,要求最小的。
int start = 0; //最优解的起点;
while(right < size) {
/* 右移动right,扩大窗口,寻找可行解 */
char c = s.charAt(right);
right++;
if(need.containsKey(c)) {
//如果是t中的字符,加入到window中
window.put(c, window.getOrDefault(c, 0) + 1);
if(window.get(c).equals(need.get(c)))
valid++;
}
/* 找到可行解后,右移动left,缩小窗口,进行优化 */
int needSize = need.size();
while(valid == needSize) {
// 不断右移动left,直到valid != needSize
c = s.charAt(left);
left++;
if(need.containsKey(c)) {
if(window.get(c).equals(need.get(c)))
valid--;
window.put(c, window.get(c) - 1);
}
/* 求最优解 [left - 1, right) */
if(valid != needSize) {
if(right - left + 1 < minLen) {
start = left - 1;
minLen = right - left + 1;
}
}
}
}
return minLen == size + 1 ? "" : s.substring(start, start + minLen);
}
}