滑动窗口模板+实际例题使用

窗口模板

具体代码
def findSubArray(nums):
    N = len(nums) # 数组/字符串长度
    left, right = 0, 0 # 双指针,表示当前遍历的区间[left, right],闭区间
    sums = 0 # 用于统计 子数组/子区间 是否有效,根据题目可能会改成求和/计数
    res = 0 # 保存最大的满足题目要求的 子数组/子串 长度
    while right < N: # 当右边的指针没有搜索到 数组/字符串 的结尾
        sums += nums[right] # 增加当前右边指针的数字/字符的求和/计数
        while 区间[left, right]不符合题意:# 此时需要一直移动左指针,直至找到一个符合题意的区间
            sums -= nums[left] # 移动左指针前需要从counter中减少left位置字符的求和/计数
            left += 1 # 真正的移动左指针,注意不能跟上面一行代码写反
        # 到 while 结束时,我们找到了一个符合题意要求的 子数组/子串
        res = max(res, right - left + 1) # 需要更新结果
        right += 1 # 移动右指针,去探索新的区间
    return res
具体思想

滑动窗口中用到了左右两个指针,它们移动的思路是:以右指针作为驱动,拖着左指针向前走。右指针每次只移动一步,而左指针在内部 while 循环中每次可能移动多步。右指针是主动前移,探索未知的新区域;左指针是被迫移动,负责寻找满足题意的区间。

  1. 定义两个指针 leftright 分别指向区间的开头和结尾,注意是闭区间;定义 sums 用来统计该区间内的各个字符出现次数;
  2. 第一重 while 循环是为了判断 right 指针的位置是否超出了数组边界;当 right 每次到了新位置,需要增加 right 指针的求和/计数;
  3. 第二重 while 循环是让 left 指针向右移动到 [left, right] 区间符合题意的位置;当 left 每次移动到了新位置,需要减少 left 指针的求和/计数;
  4. 在第二重 while 循环之后,成功找到了一个符合题意的 [left, right] 区间,题目要求最大的区间长度,因此更新 resmax(res, 当前区间的长度)
  5. right 指针每次向右移动一步,开始探索新的区间。

模板中的 sums 需要根据题目意思具体去修改,如果是计数题目,就需要改成字典用于计数。当左右指针发生变化的时候,都需要更新 sums

另外一个需要根据题目去修改的是内层 while 循环的判断条件,即: 区间[left, right]不符合题意

补充
  1. 内层while循环是为了找到使当前区间的"总成本"满足条件的left指针。也就是说, 经过这个while循环之后, 窗口内的元素满足题意, 所以可以用于更新res。
  2. 但是常常还有另一种做法, 并不使用这个内层while循环, 而是使用if, 这样不需要维护res,结果只要返回arr.size() - left即可。也就是不使用while循环的情况下, 窗口内的元素不是时时刻刻满足题意, 它只是用来记录至今为止满足题意的最大的窗口长度。
  3. 如果答案需要我们返回具体的值,而不是仅仅长度的话, 那么必须使用嵌套内层的while循环

实际例题

1208. 尽可能使字符串相等

题目思想
比如给出字符串’abcd’和’bcdf’以及cost=3,我们需要求出前串的子串若能在开销(开销是两者 ASCII 码之差的绝对值)内转换成后串的子串,所对应的最大子串长是多少(注意这里的子串是连续的,并非子序列)。我们可以直接根据两个字符串得到对应的代价数组costs = [1, 1, 1, 2],由此题目转化为求代价数组和不超过cost的最大子数组的长度,我们可以利用滑动窗口来解决

注意

  • 本题对应的模板sum含义即为整数用于求和
  • 区间[left, right]不符合题意即子数组的和大于cost

对应代码:

class Solution {
    public int equalSubstring(String s, String t, int maxCost) {
       int n=s.length();
        char[] ss=s.toCharArray();
        char[] tt=t.toCharArray();
        int[] flag=new int[n];
        for (int i = 0; i <n; i++) {
            flag[i]=Math.abs(ss[i]-tt[i]);
        }
        int sum=0,res=0,left=0,right=0;
        while(right<n){
            sum+=flag[right];

            while(sum>maxCost){
                sum-=flag[left];
                left+=1;
            }
            res=Math.max(res,right-left+1);
            right+=1;
        }
        return res;
    }
}

424. 替换后的最长重复字符

题目思路:
题意要求我们找到包含重复字母的最长子串的长度,可以利用字符转换把不是该字符的转换为是该字符,在限定的次数内,即求字符串中一个最长的区间,该区间内的出现次数较少的字符的个数不超过 k

class Solution {
    public int characterReplacement(String s, int k) {
       int len=s.length();
        if(len<2) return len;
        char[] ss=s.toCharArray();
        int left=0,right=0;
        int res=0,maxcount=0;
        int[] freq=new int[26];
        while(right<len){
            freq[ss[right]-'A']++;
            // 在这里维护 maxCount,因为每一次右边界读入一个字符,字符频数增加,才会使得 maxCount 增加
            maxcount = Math.max(maxcount, freq[ss[right] - 'A']);
            while(right-left+1>maxcount+k){
                freq[ss[left]-'A']--;
                left++;
            }
            res=Math.max(res,right-left+1);
            right++;
        }
        return res;
    }
}

1423. 可获得的最大点数

题目思路:
每次取牌只能从首尾两端去取,取够k次,计算能得到的最大和,我们可以转换为找连续的n-k张点数和最小的牌,从而利用滑动窗口来解决

class Solution {
    public int maxScore(int[] cardPoints, int k) {
        int windowssize= cardPoints.length-k;
        int sum=0,res=Integer.MAX_VALUE;
        for (int i = 0; i < cardPoints.length ; i++) {
            sum+=cardPoints[i];
            if(i>=windowssize) sum-=cardPoints[i-windowssize];
            if(i>=windowssize-1) res=Math.min(res,sum);
        }
        return Arrays.stream(cardPoints).sum()-res;
    }
}

法二: 我们可以利用presum来做,题目等价于:求从 cardPoints 最左边抽 i 个数字,从 cardPoints 最右边抽取 k - i 个数字,能抽取获得的最大点数是多少。即抽走的卡牌点数之和 = cardPoints 所有元素之和 - 剩余的中间部分元素之和。
求区间的和可以用 preSum 。

preSum 方法还能快速计算指定区间段 i ~ j 的元素之和。它的计算方法是从左向右遍历数组,当遍历到数组的 i 位置时, preSum 表示 i 位置左边的元素之和。假设数组长度为 N ,我们定义一个长度为 N+1 的 preSum 数组, preSum[i] 表示该元素左边所有元素之和(不包含当前元素)。然后遍历一次数组,累加区间 [0, i) 范围内的元素,可以得到 preSum 数组。代码如下:

int n=nums.length;
int[] presum=new int[n+1];
for(int i=0;i<n;i++) preSum[i + 1] = preSum[i] + nums[i];

利用 preSum 数组,可以在 O(1) 的时间内快速求出 nums 任意区间 [i, j] (两端都包含) 的各元素之和。

sum(i, j) = preSum[j + 1] - preSum[i];

综合以上的思路,我们的想法可以先求 preSum ,然后使用一个 0 ~ k 的遍历表示从左边拿走的元素数,然后根据窗口大小 windowSize = N - k ,利用 preSum 快速求窗口内元素之和。

class Solution {
    public int maxScore(int[] cardPoints, int k) {
        int n=cardPoints.length;
        int[] presum=new int[n+1];
        for(int i=0;i<n;i++){
            presum[i+1]=presum[i]+cardPoints[i];
        }
        int res=Integer.MAX_VALUE;
        int windowssize=n-k;
        for(int i=0;i<k+1;i++) res=Math.min(res,presum[windowssize+i]-presum[i]);
        return  Arrays.stream(cardPoints).sum()-res;
    }
}

3. 无重复字符的最长子串

题目思路:找不包含重复字符的最长子串,利用滑动窗口来做,利用哈希表(不利用提供的库,用空间换时间,时间效率百分百)判断,如果某字符数目超过1,则最左边的字符出去,滑动窗口后移,直至满足条件为止

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n=s.length();
        char[] ss=s.toCharArray();
        int left=0,right=0;
        int res=0;
        int[] freq=new int[128];
        while(right<n){
            freq[ss[right]]++;
            while(freq[ss[right]]>1){
                freq[ss[left]]--;
                left++;
            }
            res=Math.max(res,right-left+1);
            right++;
        }
        return res;

    }
}

643. 子数组最大平均数 I

本题思路:给定 n 个整数,找出平均数最大且长度为 k 的连续子数组,并输出该最大平均数。我们可以直接利用滑动窗口做,平均数最大的窗口,也是和最大的,所以我们只需要找到窗口=k的最大子数组即可

class Solution {
    public static double findMaxAverage(int[] nums, int k) {
        int n=nums.length;
        if(n==1&&k==1) return (double)nums[0];
        if(n==k) return (double)Arrays.stream(nums).sum()/k;
        int left=0,right=0,res=Integer.MIN_VALUE,sum=0;//注意res要设置为最小,而非0,因为有可能出现部分数组和都是小于0的情况,这样就得不到最小值,所以要利用Integer.MIN_VALUE。
        while(right<n){
            sum+=nums[right];

            if(right-left+1>=k+1){
                sum-=nums[left];
                left++;

            }
            if(right-left+1>=k){
                res=Math.max(sum,res);
            }
            right++;
        }
        return (double)res/k;
    }
   }

References

  1. 分享珍藏的双指针模板,动图帮助理解本题
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值