Appearance
滑动窗口技巧
算法思想
滑动窗口算法的思路是这样:
_1、_我们在字符串S
中使用双指针中的左右指针技巧,初始化left = right = 0
,把索引左闭右开区间[left, right)
称为一个「窗口」。
_2、_我们先不断地增加right
指针扩大窗口[left, right)
,直到窗口中的字符串符合要求(包含了T
中的所有字符)。
_3、_此时,我们停止增加right
,转而不断增加left
指针缩小窗口[left, right)
,直到窗口中的字符串不再符合要求(不包含T
中的所有字符了)。同时,每次增加left
,我们都要更新一轮结果。
_4、_重复第 2 和第 3 步,直到right
到达字符串S
的尽头。
现在开始套模板,只需要思考以下四个问题:
**1、**当移动right
扩大窗口,即加入字符时,应该更新哪些数据?
**2、**什么条件下,窗口应该暂停扩大,开始移动left
缩小窗口?
**3、**当移动left
缩小窗口,即移出字符时,应该更新哪些数据?
**4、**我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
一、最小覆盖子串
import java.util.HashMap;
/*
* @lc app=leetcode.cn id=76 lang=java
*
* [76] 最小覆盖子串
*/
// @lc code=start
class Solution {
public String minWindow(String s, String t) {
// 首先建立两个哈希表,存储窗口内包含的字符,以及存储需要的包含的字符
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();
for (int i = 0; i < t.length(); i++) {
char ch = t.charAt(i);
need.put(ch, need.getOrDefault(ch, 0) + 1);
}
// 建立两个指针,分别指向窗口的前后
int left = 0, right = 0;
// valid变量表示窗口中满足need条件的字符个数
int valid = 0;
// 记录最小覆盖子串的起始索引及长度
int start = 0, len = Integer.MAX_VALUE;
while (right < s.length()) {
// 获取移入窗口的字符,同时右移窗口
char ch = s.charAt(right++);
// 如果需要包含这个字符,对窗口内的数据进行一系列的更新
if (need.containsKey(ch)) {
// 更新窗口内的该字符的信息
window.put(ch, window.getOrDefault(ch, 0) + 1);
if (window.getOrDefault(ch, 0).equals(need.getOrDefault(ch, 0))) {
valid++;
}
}
// 当valid和need.size的大小相同,则说明窗口已满足条件,已经完全覆盖了串T。
while (valid == need.size()) {
// 当已经满足条件时,更新最小覆盖子串的信息
if (right - left < len) {
start = left;
len = right - left;
}
// 移除窗口的字符
char d = s.charAt(left++);
// 如果需要包含这个字符
if (need.containsKey(d)) {
if (window.getOrDefault(d, 0).equals(need.getOrDefault(d, 0))) {
// 如果当前窗口下的字符个数只能刚好满足需求,移除字符后会退出循环,
// 此时的最小覆盖子串已经暂时形成
valid--;
}
// 更新窗口内的该字符的信息
window.put(d, window.getOrDefault(d, 0) - 1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
}
// @lc code=end
需要注意的是,当我们发现某个字符在window
的数量满足了need
的需要,就要更新valid
,表示有一个字符已经满足要求。而且,你能发现,两次对窗口内数据的更新操作是完全对称的。
当valid == need.size()
时,说明T
中所有字符已经被覆盖,已经得到一个可行的覆盖子串,现在应该开始收缩窗口了,以便得到「最小覆盖子串」。
移动left
收缩窗口时,窗口内的字符都是可行解,所以应该在收缩窗口的阶段进行最小覆盖子串的更新,以便从可行解中找到长度最短的最终结果。
二、找所有字母异位词
所谓的字母异位词,不就是排列吗? 相当于,输入一个串S
,一个串T
,找到S
中所有T
的排列,返回它们的起始索引。
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/*
* @lc app=leetcode.cn id=567 lang=java
*
* [567] 字符串的排列
*/
// @lc code=start
class Solution {
public List<Integer> findAnagrams(String s1, String s2) {
Map<Character, Integer> window = new HashMap<>();
Map<Character, Integer> need = new HashMap<>();
for (int i = 0; i < s2.length(); i++) {
char ch = s2.charAt(i);
need.put(ch, need.getOrDefault(ch, 0) + 1);
}
int l = 0, r = 0;
int valid = 0;
List<Integer> list = new ArrayList<>();
while (r < s1.length()) {
char c = s1.charAt(r++);
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (need.getOrDefault(c, 0).equals(window.getOrDefault(c, 0))) {
valid++;
}
}
// 改动一:收缩条件
while (r - l >= s2.length()) {
// 改动二
if (valid == need.size()) {
list.add(l);
}
char d = s1.charAt(l++);
if (need.containsKey(d)) {
if (need.getOrDefault(d, 0).equals(window.getOrDefault(d, 0))) {
valid--;
}
window.put(d, window.getOrDefault(d, 0) - 1);
}
}
}
return list;
}
}
// @lc code=end
三、最长无重复子串
当window[c]
值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动left
缩小窗口了嘛。
唯一需要注意的是,在哪里更新结果res
呢?我们要的是最长无重复子串,哪一个阶段可以保证窗口中的字符串是没有重复的呢?
这里和之前不一样,要在收缩窗口完成后更新res
,因为窗口收缩的 while 条件是存在重复元素,换句话说收缩完成后一定保证窗口中没有重复嘛。
import java.util.HashMap;
import java.util.Map;
/*
* @lc app=leetcode.cn id=3 lang=java
*
* [3] 无重复字符的最长子串
*/
// @lc code=start
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>();
int res = 0;
int l = 0, r = 0;
while (r < s.length()) {
char c = s.charAt(r++);
map.put(c, map.getOrDefault(c, 0) + 1);
while (map.getOrDefault(c, 0) > 1) {
char d = s.charAt(l++);
map.put(d, map.getOrDefault(d, 0) - 1);
}
res = Math.max(res, r - l);
}
return res;
}
}
// @lc code=end