记得一定要看到文末,会有惊喜哦哦哦哦!!!
滑动窗口是为了解决字符串中子串相关问题,举几个例子:
s = "ADOBECODEBANC", t = "ABC"
,寻找s
中最小的t
覆盖,结果为:BANC
s1 = "ab" s2 = "eidbaooo"
,判断s2
中是否包含s1
的任一全排列序列s = "cbaebabacd", p = "abc"
,寻找s
中p
的所有全排列s = "abcabcbb"
,寻找s
中最长无重复子串上述四个例子分别对应最上面的四个题目,详情可点击题目查看
我们的思路很简单:维护一个窗口window
,不断的对窗口进行扩张和收缩,来得到最终的结果。我们接下来针对第一个例子来详细介绍滑动窗口的思想
首先我们利用左右指针来模拟一个窗口,窗口的大小为[left, right)
,注意是左闭右开
其次我们还需要一个存放我们目标结果的窗口need
,只有当window==need
的时候,才进行一次结果的更新;既然need
是存放目标结果的窗口,那么need
始终都不会发生改变
同时我们维护了一个valid
变量,根据题意,只有当valid
满足相应的条件时,才进行窗口的收缩
如上面的动图所示。下面给出相对于的代码 (建议仔细看注释)。题目最小覆盖子串
public String minWindow(String s, String t) {
// 实时记录窗口内的数据 (只存放 need 中需要的字符)
Map<Character, Integer> window = new HashMap<>();
// 记录目标字符,始终保持不变
Map<Character, Integer> need = new HashMap<>();
// 初始化 need
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
// 窗口中满足 need 目标需要的字符个数
// 即:valid == need.size() 时满足条件
int valid = 0;
// 记录最小覆盖的起始位置及长度
int start = 0, len = Integer.MAX_VALUE;
while (right < s.length()) {
// 1. 窗口扩张
char c = s.charAt(right);
right++;
// 2. 更新 valid,window 数据
// 判断此时窗口当前数据是否为 need 需要的数据,即 need 中是否存在
if (need.containsKey(c)) {
// 先更新 window
window.put(c, window.getOrDefault(c, 0) + 1);
// 后更新 valid
// 说明字符 c 的数量已经满足要求
if (window.get(c).equals(need.get(c))) {
valid++;
}
}
// 当 valid == need.size() 时,窗口开始收缩
while (valid == need.size()) {
// 1. 判断结果是否需要更新
if (right - left < len) {
start = left;
len = right - left;
}
// 2. 窗口收缩
char d = s.charAt(left);
left++;
// 3. 更新 valid,window 数据
if (need.containsKey(d)) {
// 先更新 valid
// 如果窗口内字符 d 的数量和 need 中一样,valid--
// 原因:更新后字符 d 的数量已经不满足要求,所以 valid 需要 -1
// 小技巧:判断 valid++ 和 valid-- 的条件是一样的
if (window.get(d).equals(need.get(d))) {
valid--;
}
// 后更新 window
window.put(d, window.get(d) - 1);
}
}
}
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
题目 字符串的排列
xxxxxxxxxx
public boolean checkInclusion(String s1, String s2) {
Map<Character, Integer> window = new HashMap<>();
Map<Character, Integer> need = new HashMap<>();
for (char c : s1.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1);
int left = 0, right = 0;
int valid = 0;
while (right < s2.length()) {
char c = s2.charAt(right);
right++;
if (need.containsKey(c)) {
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c))) valid++;
}
while (valid == need.size()) {
// 先判断,后收缩
// 如果窗口长度等于 s1 长度,则满足条件
if (right - left == s1.length()) return true;
// 否则,进行收缩
char d = s2.charAt(left);
left++;
// 收缩后,更新 valid,window 数据
if (need.containsKey(d)) {
// 先更新 valid
if (window.get(d).equals(need.get(d))) valid--;
// 后更新 window
window.put(d, window.get(d) - 1);
}
}
}
return false;
}
xxxxxxxxxx
public List<Integer> findAnagrams(String s, String p) {
Map<Character, Integer> window = new HashMap<>();
Map<Character, Integer> need = new HashMap<>();
// 初始化 need
for (char c : p.toCharArray()) need.put(c, need.getOrDefault(c, 0) + 1);
int left = 0, right = 0;
int valid = 0;
List<Integer> res = new ArrayList<>();
while (right < s.length()) {
char c = s.charAt(right);
right++;
// 更新 valid,window 数据
// 只有当 need 中存在时,才更新
// 记住:window 记录 need 需要的数据
if (need.containsKey(c)) {
// 先更新 window,再更新 valid,和收缩刚好相反
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c).equals(need.get(c))) valid++;
}
while (valid == need.size()) {
// 先判断
if (right - left == p.length()) res.add(left);
// 后收缩
char d = s.charAt(left);
left++;
// 更新 valid,window 数据
// 只有当 need 中存在时,才更新
// 记住:window 记录 need 需要的数据
if (need.containsKey(d)) {
// 先更新 valid,再更新 window
if (window.get(d).equals(need.get(d))) valid--;
window.put(d, window.get(d) - 1);
}
}
}
return res;
}
题目 无重复字符的最长子串
注意:这个题目和模版有少许的区别,但是核心思想是不变的,只是少了need
目标窗口而已。但是我尽量把代码和模版保持了一致
xxxxxxxxxx
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
// true: 无重复
// false: 有重复
boolean valid = true;
int res = 0;
while (right < s.length()) {
// 窗口扩张
char c = s.charAt(right);
right++;
// 更新 window,valid
window.put(c, window.getOrDefault(c, 0) + 1);
if (window.get(c) > 1) {
// 有重复
valid = false;
}
// 有重复后,开始收缩窗口
while (!valid) {
char d = s.charAt(left);
left++;
// 更新 valid 的条件和扩张时的条件保持一致
if (window.get(d) > 1) {
valid = true;
}
// 更新 window
window.put(d, window.get(d) - 1);
}
// 执行到此处,一定无重复
// 更新结果
res = Math.max(res, right - left);
}
return res;
}
好了,现在来总结一下模版吧!!哈哈哈哈哈
xxxxxxxxxx
public String slideWindow(String s, String t) {
// 实时记录窗口内的数据 (只存放 need 中需要的字符)
Map<Character, Integer> window = new HashMap<>();
// 记录目标字符,始终保持不变
Map<Character, Integer> need = new HashMap<>();
// 初始化 need 目标窗口
// code...
// 左闭右开 [left, right)
int left = 0, right = 0;
// 存放满足收缩条件的变量
// valid
while (right < s.length()) {
// 1. 窗口扩张
char c = s.charAt(right);
right++;
// 2. 更新 valid,window 数据
if (need.containsKey(c)) {
// 更新 window
// code...
// 更新 valid
if (条件) {
// code...
}
}
// 根据 valid,来判断是否需要收缩
while (valid == need.size()) {
// 1. 判断结果是否需要更新
// code...
// 2. 窗口收缩
char d = s.charAt(left);
left++;
// 3. 更新 valid,window 数据
if (need.containsKey(d)) {
// 更新 valid
if (条件) {
// code...
}
// 后更新 window
// code...
}
}
}
return res;
}