框架
Map<Character, Integer> window = new HashMap<>();
int size = s.size();
int left = 0, right = 0;
while(right < size) {
// 增大窗口
char c = s.charAt(right);
// 表示增加键为 c 的值,如果 map 中没有这个键,则返回默认值 0,然后加上 1
window.put(c, window.getOrDefault(c, 0) + 1); // 等同于c++的map[key]++
right++;
// 进行窗口内的一系列数据更新
// ...
// 注意最终代码不要留下print,因为IO很费时间
System.out.printf("window:[%d, %d]\n", left, right);
while(window needs shrink) {
// 缩小窗口
char d = s.charAt(left);
// 移出去的字符对应需要的次数减一
window.put(d, window.get(d) - 1);
left++;
// 进行窗口内的一系列数据更新
// ...
}
}
虽然滑动窗口代码框架中有一个嵌套的 while 循环,但算法的时间复杂度依然是 O(N)
,其中 N
是输入字符串/数组的长度。
为什么呢?简单来说,指针 left, right
不会回退(它们的值只增不减),所以字符串/数组中的每个元素都只会进入窗口一次,然后被移出窗口一次,不会说有某些元素多次进入和离开窗口,所以算法的时间复杂度就和字符串/数组的长度成正比。
另外,Java 中的 Integer 和 String 这种包装类不能直接用 ==
进行相等判断,而应该使用类的 equals
方法。
通用思路
第 2 步相当于在寻找一个「可行解」,然后第 3 步在优化这个「可行解」,最终找到最优解,也就是最短的覆盖子串。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动,这就是「滑动窗口」这个名字的来历。
框架的使用方法
- 初始化window和need两个哈希表,记录窗口中的字符和需要凑齐的字符。
unordered_map<char, int> need, window;
for (char c : t) need[c]++;
- 然后,使用
left
和right
变量初始化窗口的两端,不要忘了,区间[left, right)
是左闭右开的,所以初始情况下窗口没有包含任何元素:
int left = 0, right = 0;
int valid = 0;
while (right < s.size()) {
// 开始滑动
}
其中 valid
变量表示窗口中满足 need
条件的字符个数,如果 valid
和 need.size
的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T
。
要思考的问题
1、什么时候应该移动 right
扩大窗口?窗口加入字符时,应该更新哪些数据?
2、什么时候窗口应该暂停扩大,开始移动 left
缩小窗口?从窗口移出字符时,应该更新哪些数据?
3、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
如果一个字符进入窗口,应该增加 window
计数器;如果一个字符将移出窗口的时候,应该减少 window
计数器;当 valid
满足 need
时应该收缩窗口;应该在收缩窗口的时候更新最终结果。
例题
最小覆盖子串
public String minWindow(String s, String t) {
// 用于记录需要的字符和窗口中的字符及其出现的次数
Map<Character, Integer> need = new HashMap<>();
Map