目录
滑动窗口的思路非常简单,就是维护一个窗口,不断滑动,然后更新答案。算法中最难的是各种细节问题,比如如何在窗口中添加元素、更新结果。本文给出滑动窗口的代码框架,再通过几道题来套用框架。其实将框架的原理明白了,这些题闭着眼睛都能写出来。
```Java
//滑动窗口框架
public static int lengthOfLongestSubstring(String s){
HashMap<Character,Integer> window=new HashMap<>();//计数窗口
int left = 0, right = 0;
int res=0;
while (right < s.length()) {
//缩小窗口
if (window.containsKey(s.charAt(right))) {
left=Math.max(window.get(s.charAt(right))+1,left);
}
// 增大窗口
window.put(s.charAt(right),right);
//更新结果
res=Math.max(res,right-left+1);
right++;
}
return res;
}
}
```
1.无重复字符的最长子串
leetcode3题
要求:给定⼀个字符串 s,请你找出其中不含有重复字符的最⻓⼦串的⻓度。
基本思路:当窗口有重复元素的时候,窗口左指针收缩到最近相同元素的下一个元素,没有重复元素右指针右移,有了新的窗口之后重新更新结果。
本道题是最简单的滑动窗口题,具体解法在框架中已经给出一种。只针对本题,window哈希表中存储的值可以是字符的索引,这样左指针就不需要每次移动一步。window哈希表中存储的值也可以是字符出现的数量,如果右指针指向的字符在哈希表中大于1,左指针指向的字符在哈希表中的值减1,左指针右移,直到右指针指向的字符在哈希表中不大于1。
就本题而言后者肯定比前者效率低,因为每次只移动一个字符。但后者在window中能记录字符的数量。
```Java
public static int lengthOfLongestSubstring(String s){
HashMap<Character,Integer> window=new HashMap<>();//计数窗口
int left = 0, right = 0;
int res=0;
while (right < s.length()) {
char c=s.charAt(right);
// 增大窗口
if(window.get(c)==null){
window.put(c,0);
}
window.put(c,window.get(c)+1);
//缩小窗口
while (window.get(c)>1) {
window.put(s.charAt(left),window.get(s.charAt(left))-1);
left++;
}
res=Math.max(res,right-left+1);
right++;
}
return res;
}
```
2.最小覆盖子串
leetcode76题
要求:给你⼀个字符串 s 、⼀个字符串 t,返回 s 中涵盖 t 所有字符的最⼩⼦串;如果 s 中不存在涵盖 t 所有字符的⼦串,则返回空字符串 ""。
解题思路:这道题可以用上题中的第二种框架,定义两个哈希表,一个记录窗体中的字符个数,一个记录t中的字符个数,当窗体可以覆盖子串的时候,移动左指针直至窗体不可以覆盖,再接着移动右指针。
现在开始套模板,只需要思考以下四个问题:
1、当移动right扩大窗口,即加入字符时,应该更新哪些数据?
2、什么条件下,窗口应该暂停扩大,开始移动left缩小窗口?
3、当移动left缩小窗口,即移出字符时,应该更新哪些数据?
4、我们要的结果应该在扩大窗口时还是缩小窗口时进行更新?
1. 本题window只计数need中有的字符,加入字符更新字符的个数
2. 当window与need中所有相同字符的个数相同,即可以覆盖时窗口停止扩大,开始缩小窗口
3. 每次移除字符,如果该字符window中存在,则计数减1,当该字符的window中计数值等于need中的数量,valid减1,因为减完1之后,该字符的计数减1会不满足覆盖。重复步骤1加入字符串。
4. 我们要的结果是在缩小窗口时更新,因为缩小窗口时满足覆盖条件。
```Java
//2.最小覆盖子串
public static String minCoSubSring(String s,String t){
HashMap<Character,Integer> window=new HashMap<>();//窗口计数
HashMap<Character,Integer> need=new HashMap<>();//t字符串计数
//统计t字符串中不同字符的个数
for(int i=0;i<t.length();i++){
char c=t.charAt(i);
if(need.get(c)==null){
need.put(c,0);
}
need.put(c,need.get(c)+1);
}
//滑动窗口
int left = 0, right = 0;
int valid=0;//满足need条件的字符个数
int start = 0, len = s.length()+1;//记录起始索引和长度
while (right < s.length()) {
char c=s.charAt(right);
// 增大窗口
//数据更新
if(need.containsKey(c)){
if(window.get(c)==null){
window.put(c,0);
}
window.put(c,window.get(c)+1);
if(window.get(c).intValue()==need.get(c).intValue()){
valid++;//满足need条件的字符
}
}
//更新输出:在扩大窗口满足条件之后
//缩小窗口:满足need条件的字符的数量等于need中字符的种类
while (valid==need.size()) {
//更新输出:在扩大窗口满足条件之后
if(right-left+1<len){//保证是最小覆盖子串
start=left;
len=right-left+1;
}
char d=s.charAt(left);
if(window.containsKey(d)){
if(window.get(d).intValue()==need.get(d).intValue()){
valid--;
}
window.put(d,window.get(d)-1);
}
left++;
}
right++;
}
return len==s.length()+1?"":s.substring(start,start+len);
}
```
3.字符串排列
leetcode567题
要求:给定两个字符串t和s,写一个函数来判断s中是否包含t的排列,换句话来说,第一个字符串的排列之一是第二个字符串的子串。
基本思路:对于这道题基本和最小覆盖一样,只有两个条件有点变化,第一,缩小窗口的时机是窗口大小等于t.size()时,即窗口中的长度等于t中的长度,第二,当发现valid==need.size()时,说明窗口中就有一个合法的排列,所以立即返回true。
至于如何处理窗口的扩大和缩小,和最小覆盖子串完全相同。
```Java
public static boolean checkInclusion(String t,String s){
HashMap<Character,Integer> window=new HashMap<>();//窗口计数
HashMap<Character,Integer> need=new HashMap<>();//t字符串计数
//统计t字符串中不同字符的个数
for(int i=0;i<t.length();i++){
char c=t.charAt(i);
if(need.get(c)==null){
need.put(c,0);
}
need.put(c,need.get(c)+1);
}
//滑动窗口
int left = 0, right = 0;//定义左右指针
int valid=0;//满足need条件的字符个数
while (right < s.length()) {
char c=s.charAt(right);
// 增大窗口
if(need.containsKey(c)){
if(window.get(c)==null){
window.put(c,0);
}
window.put(c,window.get(c)+1);
if(window.get(c).intValue()==need.get(c).intValue()){
valid++;//满足need条件的字符
}
}
//缩小窗口:重排列长度肯定得一样
while (right-left+1==t.length()) {
//更新输出:长度一样之后有满足和不满足重排列两种情况
if(need.size()==valid){//满足重排列
return true;
}
//窗口收缩
char d=s.charAt(left);
if(window.containsKey(d)){
if(window.get(d).intValue()==need.get(d).intValue()){
valid--;
}
window.put(d,window.get(d)-1);
}
left++;
}
right++;
}
return false;
}
```
熟练框架之后是不是觉得这种难题瞬间变简单了,只要分析清楚窗口扩大window、valid怎么更新,满足什么条件窗口缩小,窗口搜小window、valid怎么更新,最终得输出结果是在扩大窗口满足条件之后更新,还是缩小窗口之后满足条件更新。
4.找到字符串中所有字母异位词
leetcode438题
要求:给定两个字符串s和p,找到s中所有p的异位词子串,返回这些子串的起始索引。不考虑答案输出的顺序。异位词指由相同字母重排列形成的字符串(包括相同的字符串)。
聪明的同学可能就发现了异位词不就是重排列嘛,当满足重排列的条件之后记录子串的起始索引即可。
```Java
public List<Integer> findAnagrams(String s, String p){
HashMap<Character,Integer> window=new HashMap<>();
HashMap<Character,Integer> need=new HashMap<>();
List<Integer> flag=new ArrayList<>();
//统计p中字符的个数
for(int i=0;i<p.length();i++){
char c=p.charAt(i);
if(need.get(c)==null){
need.put(c,0);
}
need.put(c,need.get(c)+1);
}
//滑动窗口
int left=0,right=0;
int valid=0;
while(right<s.length()){
//增大窗口
char c=s.charAt(right);
if(need.containsKey(c)){
if(window.get(c)==null){
window.put(c,0);
}
window.put(c,window.get(c)+1);
if(window.get(c).intValue()==need.get(c).intValue()){
valid++;
}
}
//缩小窗口
while(right-left+1==p.length()){
if(valid==need.size()){
flag.add(left);
}
char d=s.charAt(left);
if(window.containsKey(d)){
if(window.get(d).intValue()==need.get(d).intValue()){
valid--;
}
window.put(d,window.get(d)-1);
}
left++;
}
right++;
}
return flag;
}
```
以上答案是不参考上面的答案仅凭框架思维写出来的,非常快速,弄懂原理之后无需记忆无需背诵,随随便便手撕代码。
5.滑动窗口最大值
leetcode239题
要求:给你⼀个整数数组 nums,有⼀个⼤⼩为 k 的滑动窗⼝从数组的最左侧移动到数组的最右侧,返回滑动窗⼝中的最⼤值。
对于本题不需要计数窗口,只需要一个长度为k的单调队列来保存窗口中的数。其它的按照框架来写即可。
```Java
// 实现单调队列
static class MonotonicQueue{
LinkedList<Integer> q = new LinkedList<>();
public void push(int n){
//将小于n的元素全部删除
while(!q.isEmpty()&&q.getLast()<n){
q.pollLast();
}
//然后将n加入尾部
q.addLast(n);
}
public void pop(int n){
if(n==q.getFirst()){//n在队列中就删除,也有可能n不是最大值被覆盖了
q.pollFirst();
}
}
public int max(){return q.getFirst();}
}
public static int[] maxSlidingWindow(int[] nums,int k){
MonotonicQueue window=new MonotonicQueue();
List<Integer> res=new ArrayList<>();
int left=0,right=0;
while (right<nums.length){
//增大窗口
int a=nums[right];
window.push(a);
//缩小窗口
while(right-left+1==k){
//更新结果
res.add(window.max());
window.pop(nums[left]);
left++;
}
right++;
}
//需要转成 int[]数组返回
int[] arr=new int[res.size()];
for(int i=0;i<res.size();i++){
arr[i]=res.get(i);
}
return arr;
}
```