一.适用场景
滑动窗口适用于求连续子序列问题,连续是关键字,一旦在题目描述中看到子序列是可以不连续的,那么就可以大胆排除滑动窗口这个方法了,力扣上常见的例题有的题目最长不含重复字母的子序列,最小覆盖子串之类的问题。
二.用法
滑动窗口的代码比较模版化,用下标i和j分别记录滑动窗口的左右边界,然后需要弄清楚下列几个问题即可。
什么时候滑动窗口需要变大,也就是说右边界需要往右延伸,往右延伸的时候需要更新什么数据。
什么时候滑动窗口需要停止变大,也就是说右边界需要停止往右延伸。
什么时候滑动窗口需要缩小,也就是左边界需要往右收缩,往右收缩时需要更新什么数据。
三.例题
先来道简单的例题练练手。
题目比较好理解,这里主要是判断题目要求的是连续子序列,那么就可以初步判断使用滑动窗口。
当窗口包含待读取字符,说明重复,窗口开始收缩,左边界开始往右移动,
当窗口不包含待读取字符,说明没重复,窗口继续扩张,右边界继续往右移动,每次扩张都记录一下。
可以看到这道题目需要我们经常判断是否重复,那么判断重复常见方法的有数组和HashSet。
数组适用于元素少且容易转换成下标的情况,比如说元素只有26个字母,那么下标就可以通过-‘a’获取到,但题目给的s是不仅仅包括字母的,那么只能用HashSet了。
最终写出来的代码如下所示。
class Solution {
public int lengthOfLongestSubstring(String s) {
if (s==null){
return -1;
}
HashSet<Character> set=new HashSet<>();
int i=0;//左边界
int j=0;//右边界
int len=s.length();
char[] chars=s.toCharArray();
int res=0;
while (j<len){
while (set.contains(chars[j])){
//重复则收缩
set.remove(chars[i]);
i++;
}
set.add(chars[j]);
j++;
res=Math.max(res,j-i);
}
return res;
}
}
再来看一道难一点但却更标准的滑动窗口例题。
最小覆盖自串,困难题,好家伙,困难题是我能想的吗,现在我连困难题都敢想了,那以后我想做什么难度的题我都不敢想。
言归正传,这道题还是比较好分析的。
由于题目需要返回一个覆盖t所有字符的最短子字符串,那么我们就需要用start字段记录该子字符串的起始下标,此外,由于题目要求是最短,所以我们还需要一个len值记录此刻覆盖t所有字符的最短子字符串的长度,一旦有新符合题目要求的子字符串出现,就需要将新字符串的长度与len值比较,如果len值更大,那么就更新len值为新子字符串的长度,同时更新left值。
当滑动窗口不包含字符串t的所有字符时,滑动窗口扩大,右边界往右移动。
当滑动窗口包含字符串t的所有字符时,滑动窗口收缩,左边界往右移动,每收缩一次就进行比较,具体比较可见上文,这里不再赘述。
最终代码如下所示。
class Solution {
public String minWindow(String s, String t) {
if (s==null||t==null){
return new String();
}
HashMap<Character,Integer> need=new HashMap<>();//记录需要覆盖的子字符串的字母个数
HashMap<Character,Integer> window=new HashMap<>();//记录窗口中包含子字符串中字母的个数
char[] sChar=s.toCharArray();
char[] tChar=t.toCharArray();
for (char c:tChar){
need.put(c,need.getOrDefault(c,0)+1);
}
int left=0,right=0,start=0,valid=0,len=Integer.MAX_VALUE;
//valid为窗口包含子字符串字母种类个数
while (right<sChar.length){
char rChar=sChar[right];
right++;
if (need.containsKey(rChar)){
window.put(rChar,window.getOrDefault(rChar,0)+1);
if (window.get(rChar).equals(need.get(rChar))){
valid++;
}
}
while (valid==need.size()){
//窗口包含子字符串的所有字母,开始收缩
if (right-left<len){
len=right-left;
start=left;
}
char lChar=sChar[left];
left++;
if (need.containsKey(lChar)){
if (window.get(lChar).equals(need.get(lChar))){
valid--;
}
window.put(lChar,window.get(lChar)-1);
}
}
}
return len==Integer.MAX_VALUE ? "":new String(sChar,start,len);
}
}
四.错误示范
前面提到过,看到题目有要求连续子序列,就可以往滑动窗口方向去想,所以当我看到这下面这道题目时,下意识就往滑动窗口想。
连续子序列,符合。
当窗口内的左右括号数目相等,就判断括号是否有效,有效就记录长度。
当窗口内右括号多于左括号,窗口应该收缩了。
当窗口内左括号数量大于右括号时,说明窗口应该继续往右扩。
嗯,一套分析下来头头是道,我真的是聪明绝顶,啪,很快啊,我就写出了下面的代码出来
class Solution {
public int longestValidParentheses(String s) {
if (s==null){
return -1;
}
int lK=0;//左括号数目
int rK=0;
int len=s.length();
int left=0;//窗口左边界
int right=0;
int res=0;
char[] sChar=s.toCharArray();
while (right<len){
if (sChar[right]=='('){
lK++;
}else {
rK++;
}
right++;
if (lK==rK&&isValid(sChar,left,right)){
res=Math.max(res,right-left);
}
while (lK<rK){
if (sChar[left]=='('){
lK--;
}else {
rK--;
}
left++;
}
}
```
return res;
}
private boolean isValid(char[] chars,int i,int j){
//判断括号是否有效
}
}
自信满满的我一看代码通过了题目所给三个测试用例就果断提交了,果真就一通操作猛如虎,一看战绩零杠五。
冷静分析了一下。
少分析了一种情况,当左括号的数量大于右括号的时候,其实是有两种可能性的。
第一种情况是左括号的数目多了,这时候需要窗口缩小。
第二种情况是右括号的数目少了,这时候需要窗口往右扩。
我只考虑了第二种情况,没有考虑到第一种情况,这时候可能就会有聪明的小伙伴要问了,那你把第一种情况考虑进去不就行了吗?
理论上是如此,但是前面提到过,当右括号多于左括号时,窗口也会进行收缩,那么就会出现只要左右括号不相等就收缩,相等就判断是否有效的情况,这是不对的,随便举一个用例。
用例:“()”
上述最长有效括号是2,但是如果按照上述方法计算出来的结果会是0。
可以发现将滑动窗口强行应用于上述题目出现的问题是收缩或者扩大窗口的条件不唯一,所以当出现这种情况时,就可以放弃使用滑动窗口了。
这道题其实应该用动态规划来解决,但这不是本文的重点,就按过不提。
参考资料
1.《l..籍》(由于该参考资料抄袭的争议比较大,就不放全名了)