title: 滑动窗口
copyright: true
toc: true
tags:

  • 滑动窗口
  • 算法
  • Java
    categories: 计算机
    declare: true
    mathjax: true
    abbrlink: 50cab45
    date: 2021-02-02 10:10:20

滑动窗口

双指针同类问题:

「力扣」第 3 题:无重复字符的最长子串;
「力扣」第 209 题:长度最小的子数组;
「力扣」第 76 题:最小覆盖子串;
「力扣」第 438 题:找到字符串中所有字母异位词;
「力扣」第 567 题:字符串的排列。

初识

1、题目:替换后的最长重复字符串

同类问题:

「力扣」第 1004 题:最大连续 1 的个数 III;
「力扣」第 1208 题:尽可能使字符串相等;
「力扣」第 1493 题:删掉一个元素以后全为 1 的最长子数组。

给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。

注意:字符串长度 和 k 不会超过 滑动窗口 - 图1

示例 1:

  1. 输入:s = "ABAB", k = 2
  2. 输出:4
  3. 解释:用两个'A'替换为两个'B',反之亦然。

示例 2:

  1. 输入:s = "AABABBA", k = 1
  2. 输出:4
  3. 解释:
  4. 将中间的一个'A'替换为'B',字符串变为 "AABBBBA"
  5. 子串 "BBBB" 有最长重复字母, 答案为 4

2、题解——什么是滑动窗口

解题思路

  • 本题是比较典型的滑动窗口问题
  • 这类问题一般通过一个滑动窗口就能在 O(N) 的时间复杂度下求解

本题可以先退化成考虑滑动窗口 - 图2 的情况,此时题目就变成了求解字符串中最长连续子串长度问题了。
我们先可以通过这个特例先了解一下滑动窗口的求解过程

滑动窗口 - 图3

上图的求解过程展示中,窗口从左至右不断扩张/滑动,当窗口触达字符串末尾字符时,运算结束,窗口的宽度为最终结果。初始窗口的宽度为 1,我们不断的通过向当前窗口覆盖的子串后面追加一个字符看是否能满足我们的要求,如果满足窗口扩张,如果不满足,窗口向右滑动。

滑动窗口 - 图4 时,子串的条件变成了允许我们变换子串中的 滑动窗口 - 图5 个字符使其变成一个连续子串

那么这个题的关键点就是我们如何判断一个字符串改变 滑动窗口 - 图6个字符,能够变成一个连续串

如果当前字符串中的出现次数最多的字母个数 滑动窗口 - 图7大于串长度,那么这个串就是满足条件的

我们维护一个数组 int[26] 来存储当前窗口中各个字母的出现次数,滑动窗口 - 图8表示窗口的左边界,滑动窗口 - 图9 表示窗口右边界

窗口扩张:滑动窗口 - 图10不变,滑动窗口 - 图11
窗口滑动:滑动窗口 - 图12滑动窗口 - 图13
滑动窗口 - 图14保存滑动窗口内相同字母出现次数的 历史 最大值,通过判断窗口宽度 滑动窗口 - 图15#card=math&code=%28right%20-%20left%20%2B%201%29)是否大于 滑动窗口 - 图16来决定窗口是否做滑动,否则窗口就扩张。

3、参考链接

作者:migoo
网址链接
来源:力扣(LeetCode)

4、代码

  1. class Solution {
  2. public int characterReplacement(String s, int k) {
  3. int[] map = new int[26]; //记录窗口中的不同字符的数量
  4. int left = 0 , right = 0, hisMaxLength = 0;
  5. for (right = 0 ; right < s.length() ; right++){ //注意right在何处增加,以及因此right-left的值是否要加1
  6. int index = s.charAt(right) - 'A';
  7. map[index]++;
  8. hisMaxLength = Math.max(hisMaxLength,map[index]);
  9. if (right - left + 1 > hisMaxLength + k){
  10. map[s.charAt(left)-'A'] --; //当窗口移动时,其中记录的字符数量也应该发生改变
  11. left++;
  12. }
  13. }
  14. return s.length()-left;
  15. }
  16. }

训练

1、题1004

class Solution {
    public int longestOnes(int[] A, int K) {
        int[] map = new int[2];
        //由于其只有0,1,故直接记录0的数量即可countZero
        int left = 0 , right = 0 , countZero = 0;
        while(right < A.length){
            int index = A[right];
            map[index]++;
            countZero = map[0];

            if( countZero > K){
                map[A[left]] -- ;
                left++;
            }
            right ++;
        }
        return A.length-left;
    }
}

2、题567-字符串的排列

一、题目描述

给定两个字符串 s1s2,写一个函数来判断 s2 是否包含 s1 的排列。换句话说,第一个字符串的排列之一是第二个字符串的子串。

二、题解

本题是类似一个==“最长连续子数组”==的问题,这里不过是将长度固定,所以可以使用滑动窗口(双指针)来解决该题:

由于排列不会改变字符串中每个字符的个数,所以只有当两个字符串每个字符的个数均相等时,一个字符串才是另一个字符串的排列。

根据这一性质,记滑动窗口 - 图17 的长度为 滑动窗口 - 图18,我们可以遍历滑动窗口 - 图19中的每个长度为 滑动窗口 - 图20 的子串,判断子串和 滑动窗口 - 图21中每个字符的个数是否相等,若相等则说明该子串是 滑动窗口 - 图22 的一个排列。

使用两个数组 滑动窗口 - 图23滑动窗口 - 图24滑动窗口 - 图25统计 滑动窗口 - 图26 中各个字符的个数, 滑动窗口 - 图27统计当前遍历的滑动窗口 - 图28 子串中各个字符的个数。

由于需要遍历的子串长度均为 滑动窗口 - 图29,我们可以使用一个固定长度为滑动窗口 - 图30 的滑动窗口来维护 滑动窗口 - 图31:滑动窗口每向右滑动一次,就多统计一次进入窗口的字符,少统计一次离开窗口的字符。然后,判断 滑动窗口 - 图32是否与 滑动窗口 - 图33相等,若相等则意味着 滑动窗口 - 图34的排列之一是 滑动窗口 - 图35的子串。

public boolean solution1(String s1, String s2){
        int n = s1.length();
        int m = s2.length();
        if (n > m ) return false;
        int[] count1 = new int[26];
        int[] count2 = new int[26];
        for (int i = 0 ; i < n; i++){
            count1[s1.charAt(i)-'a']++;
            count2[s2.charAt(i)-'a']++;
        }
        if (Arrays.equals(count1,count2)){
            return true;
        }
        for (int i = n ; i < m ; i++){
            count2[s2.charAt(i)-'a']++;
            count2[s2.charAt(i-n)-'a']--;
            if (Arrays.equals(count1,count2)){
                return true;
            }
        }
        return  false;
    }

但是该方法中多次使用Arrays.equals(),可以采用以下思路进行简化:

注意到每次窗口滑动时,只统计了一进一出两个字符,却比较了整个 滑动窗口 - 图36滑动窗口 - 图37数组。

从这个角度出发,我们可以用一个变量滑动窗口 - 图38来记录 滑动窗口 - 图39滑动窗口 - 图40的不同值的个数,这样判断滑动窗口 - 图41滑动窗口 - 图42是否相等就转换成了判断 滑动窗口 - 图43 是否为滑动窗口 - 图44

每次窗口滑动,记一进一出两个字符为滑动窗口 - 图45滑动窗口 - 图46

滑动窗口 - 图47则对滑动窗口 - 图48无影响,可以直接跳过。

滑动窗口 - 图49对于字符滑动窗口 - 图50,在修改 之前滑动窗口 - 图51若有 滑动窗口 - 图52,则将 滑动窗口 - 图53 加一;在修改 滑动窗口 - 图54之后若有滑动窗口 - 图55,则将 滑动窗口 - 图56减一。字符滑动窗口 - 图57同理。

此外,为简化上述逻辑,我们可以只用一个数组滑动窗口 - 图58,其中滑动窗口 - 图59,将 滑动窗口 - 图60滑动窗口 - 图61的比较替换成滑动窗口 - 图62滑动窗口 - 图63 的比较。

public boolean solution2(String s1, String s2){
        //solution1中频繁使用Arrays.equals()来进行数组比较,浪费时间,可以使用diff记录两者的不同个数
        int n = s1.length();
        int m = s2.length();
        if (n > m ) return false;
        int diff = 0;
        int[] count = new int[26];
        for (int i = 0 ; i < n; i++){
            count[s1.charAt(i)-'a']--;
            count[s2.charAt(i)-'a']++;

        }
        for (int i : count){
            if(i!=0) diff++;
        }
        if (diff == 0) return true;

        for (int i = n ; i < m ; i++){
            int x = s2.charAt(i)-'a' , y = s2.charAt(i-n) - 'a';
            if(x==y) continue;
            if (count[x] == 0) diff++;
            count[x]++;
            if (count[x] == 0) diff--;
            if (count[y] == 0) diff++;
            count[y]--;
            if (count[y]==0) diff--;
            if (diff == 0 ) return true;
        }
        return false;
    }