滑动窗口实际上是通过双指针实现的,[left,right]之间的范围就是窗口。通常用于解决字符串、数组相关的问题。比如最小子串等。
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 ...;
}
关键是判断收缩以及扩展时的情况,如何向窗口中添加新元素,如何缩小窗口,在窗口移动的哪个阶段更新结果。
给你一个字符串S、一个字符串T,请在字符串S里面找出:包含T所有字母的最小子串
输入:S=“ADOBECODEBANC”, T=“ABC”
输出:“BANC”
思路:
维护好need和window两个Map,用于记录窗口内的元素,每次窗口扩展后,比较need和window就可以知道是否含有相同字母。
设置双指针,left和right。
right一直增大,直到窗口[left,right)满足要求包含字符串T为止。
left一直增大,直到窗口中的字符串不再满足条件。每次增加left都要更新一轮结果
重复3、4两步,直到right到尽头
可以理解为步骤2在找可行解,步骤3在优化这个解。
public static String minWindow(String s, String t) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = 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++;
// 进行窗口内数据的一系列更新
if (need.containsKey(c)) {
int tmp = window.getOrDefault(c, 0);
window.put(c, ++tmp);
if (window.get(c) == need.get(c))
valid++;
}
// 判断左侧窗口是否要收缩
while (valid == need.size()) {
// 在这里更新最小覆盖子串
// len用于记录当前最佳的长度
if (right - left < len) {
start = left;
len = right - left;
}
// d 是将移出窗口的字符
char d = s.charAt(left);
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.containsKey(d)) {
if (window.get(d) == need.get(d))
valid--;
int tmp = window.get(d);
window.put(d, --tmp);
}
}
}
// 返回最小覆盖子串
return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
}
给定两个字符串s1和s2,写一个函数来判断s2是否包含s1的排列。
输入:s1=“ab”, s2=“eidbaooo”
输出:True
因为"ba"在s2中
思路:
思路同上一道题,但是这道题中需要一直维持窗口大小为字符串s1的长度,这也是扩展和收缩的条件。
public static boolean checkInclusion(String s, String t) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = 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++;
// 进行窗口内数据的一系列更新
if (need.containsKey(c)) {
int tmp = window.getOrDefault(c, 0);
window.put(c, ++tmp);
if (window.get(c) == need.get(c))
valid++;
}
// 判断左侧窗口是否要收缩
while (right-left>=t_len) {
// 在这里更新最小覆盖子串
if (valid==need.size()) {
return true;
}
// d 是将移出窗口的字符
char d = s.charAt(left);
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.containsKey(d)) {
if (window.get(d) == need.get(d))
valid--;
int tmp = window.get(d);
window.put(d, --tmp);
}
}
}
// 返回最小覆盖子串
return false;
}
此题的解进在判断左侧窗口是否要收缩的地方有了较大的改变,其他地方基本一致,可见框架是很有用的。
给定一个字符串s和一个非空字符串p,找到s中所有是p的字母异位词的子串,返回这些子串的起始索引
输入:s:“cbaebabacd”, p:“abc”
输出:[0,6]
思路:
这一题的思路与上一题是一模一样的,也是要维护好固定大小的窗口,需要做的改动就是需要判断一下必须是异位词,不能和字符串p一模一样。
public static ArrayList<Integer> checkInclusion(String s, String t) {
HashMap<Character, Integer> need = new HashMap<>();
HashMap<Character, Integer> window = new HashMap<>();
ArrayList<Integer> res = new ArrayList<>();
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++;
// 进行窗口内数据的一系列更新
if (need.containsKey(c)) {
int tmp = window.getOrDefault(c, 0);
window.put(c, ++tmp);
if (window.get(c) == need.get(c))
valid++;
}
// 判断左侧窗口是否要收缩
while (right-left>=t_len) {
// 在这里更新最小覆盖子串
if (valid==need.size()&&!s.substring(left,right).equals(t)) {
res.add(left);
}
// d 是将移出窗口的字符
char d = s.charAt(left);
// 左移窗口
left++;
// 进行窗口内数据的一系列更新
if (need.containsKey(d)) {
if (window.get(d) == need.get(d))
valid--;
int tmp = window.get(d);
window.put(d, --tmp);
}
}
}
// 返回最小覆盖子串
return res;
}
给定一个字符串,请你找出其中不含有重复字符的最长子串的长度
输入:“abcabcbb”
输出:3
思路:
这一题与第一题的思路一样,窗口大小不固定,一直扩展直到不符合条件出现重复字符为止,然后开始收缩,直到又满足条件了。反复重复,记得更新最新长度。
public static int lengthOfLongestSubstring(String s) {
HashMap<Character, Integer> window = new HashMap<>();
int res = 0;
int left = 0, right = 0;
while (right < s.length()) {
// c 是将移入窗口的字符
char c = s.charAt(right); // 右移窗口
int count = window.getOrDefault(c, 0);
// 进行窗口内数据的一系列更新
window.put(c, ++count);
right++;
// 判断左侧窗口是否要收缩
while (window.getOrDefault(c, 0)>1) {
count = window.get(s.charAt(left));
// 将字符移除
window.put(s.charAt(left), --count);
// 左移窗口
left++;
}
if(res<right-left){
res= right-left;
}
}
// 返回最小覆盖子串
return res;
}
滑动窗口问题的关键:
在框架中需要灵活更改的几个地方也对应了提到的几个关键点。
申明:本博文是看了labuladong的算法小抄之后个人的理解以及总结。