滑动窗口(java)

滑动窗口是双指针的一种应用,它的特点就是窗口要么扩张,要么向右滑动,不存在窗口缩小的情况。这样当O(N)遍历一遍后,窗口的长度就是要求的最大值。

先看一道最简单的滑动窗口题,给定一串字符串(只包含大写字母),求最长连续序列的长度。这道题用普通的双指针或者别的做法都能做,但是这里用滑动窗口演示一下:

例:ABAA----2 AAAA----4 ACDBB----2 …

开始这两步和普通的双指针没什么区别,left不动,right+1,代表窗口扩张,但是当碰到 “B” 的时候,我们并没有让left直接移到 “B” 的位置,这是普通双指针的做法;而是让left和right同时+1,代表窗口向右滑动。

这时窗口内的序列为 “AB” 不符合连续的要求,接着向右滑动。这也是滑动窗口和普通双指针的区别,普通双指针维护的是连续序列,滑动窗口只记录最长的序列,因为前面已经有 "AA"了,碰到比这个短的连续序列可以直接无视,接着滑动。

这个时候窗口内的序列 “BB” 满足连续的要求,可以先扩张窗口,如果扩张成功,就不动left;如果扩张不成功再left+1,相当于让窗口向右滑动。

此时滑动窗口已经滑到最右边了,滑动窗口的长度就是最长的连续序列长度,可以看到滑动窗口内此时并不是连续序列,因为滑动窗口不会收缩,遇见过最长的序列,这些短的序列就看不上眼了。

图看起来比较直观,但是要用代码实现需要解决一个核心问题: 怎么判断窗口内的序列是连续的? 不管是窗口扩张后,还是窗口滑动后,我们都需要判断当前窗口内的序列是不是连续的,总不能从left开始再数一遍吧,这就失去了滑动窗口的意义了。

注意到两个核心点后,可能思路会清晰一点:

  • 序列只有26个大写字母,可以用哈希表,或者说计数表,思想是一样的:int[] count = new int[26]。专门用来记录窗口内各个字母的个数
  • 无论是窗口扩张,还是窗口滑动,right都是+1;left只有在滑动的时候才会+1。

那怎么说明窗口内的序列是连续的呢?只要count中最大值等于窗口长度,就说明是连续的。但是怎么求count中最大的值呢?这又是一个问题,总不能每次从头到尾遍历count数组吧,这就太慢了。

这时第二点就派上用场了,right每次都+1,那么right位置的字母个数要+1,"可能"让最大个数发生变化的就只能是这个字母了。我们只需要让扩张之前的最大值和这个字母的个数比就行了。

可能有人会有疑惑了,那left有时候还要+1呢,这个动作也会改变count啊。这时就要再想一下上面的那句话了,我们是先让窗口扩张的,如果扩张之后不满足连续的要求,就把left位置的字母个数-1,代表这个字母从窗口中减少了一个,然后left+1。也就是我们在比count的最大值的时候,left的位置的字母还没被踢出去呢,当然不会影响我们找最大值。单看文字可能比较干,下面举一个简单的例子:

比如现在窗口内是 “BB”,向右扩张,变成 “BB*” ,这个 * 的个数就是我们要比的,如果这个 * 是 B,那么 2 和 3 比,最大值就变3了;如果这个 * 是别的字母,那 2 和 1比还是2,2 < 3 不连续,需要让窗口向右滑动,那么 left+1 之前先把 B 的个数减一,这样窗口滑动之后变成 “BA” ,B 的个数是 1,A 的个数也是 1。

理解了这个例子就可以开始写代码了:

public int solution(String s) {
    if (s == null || s.length() == 0)
        return 0;
    
    int left = 0, right = 0;
    int maxCount = 0;
    int[] count = new int[26];
    while (right < s.length()) {
        // 窗口最右边的字母个数+1
        count[s.charAt(right) - 'A']++;
        // 找当前窗口中字母个数的最大值
        maxCount = Math.max(maxCount, count[s.charAt(right) - 'A']);
        // 最大值小于窗口长度,说明不连续,需要窗口滑动
        if (maxCount < right - left + 1) {
            count[s.charAt(left) - 'A']--;
            left++;
        }
        // 窗口扩张
        right++;
    }
    // 遍历一遍后,窗口长度就是最长的序列长度
    return right - left;
}

下面再看今天的每日一题,也是这道题的变形:

leetcode424:

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

猛一看好像和上面的题没啥关系,但是其实上面的代码只改一行,就是这道题的答案。这里无非是判断连续的条件宽松了一点,即有 k 次错的机会,窗口内的序列可以不连续,只要错的字母个数小于等于k,我还可以认为你是连续的

那只需要改if的条件就可以了,之前是maxCount < right - left + 1就要滑动窗口,现在maxCount + k < right - left + 1才需要滑动窗口,都给你 k 次机会了,结果还是不连续,那就只能滑动窗口了。

滑动窗口模板题

leetcode643:给定 n 个整数,找出平均数最大且长度为 k 的连续子数组,并输出该最大平均数。

输入:[1,12,-5,-6,50,3], k = 4
输出:12.75
解释:最大平均数 (12-5-6+50)/4 = 51/4 = 12.75

public double findMaxAverage(int[] nums, int k) {
    int left = 0, right = 0;
    int ans = Integer.MIN_VALUE;
    int sum = 0;
    while (right < nums.length) {
        sum += nums[right];
        if (right >= k - 1) {
            ans = Math.max(ans, sum);
            sum -= nums[left];
            left++;
        }
        right++;
    }
    return ans / (double) k;
}

leetcode1208

给你两个长度相同的字符串,s 和 t。

将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。

用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。

如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。

如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。
示例:
输入:s = “abcd”, t = “bcdf”, cost = 3
输出:3
解释:s 中的 “abc” 可以变为 “bcd”。开销为 3,所以最大长度为 3。

public int equalSubstring(String s, String t, int maxCost) {
    int left = 0, right = 0;
    int sum = 0;
    int n = s.length();
    while (right < n) {
        sum += Math.abs(s.charAt(right) - t.charAt(right));
        if (sum > maxCost) {
            sum -= Math.abs(s.charAt(left) - t.charAt(left));
            left++;
        }
        right++;
    }
    return right - left;
}

上面两个题都是不折不扣的模板题,没有任何套路,滑动窗口的核心代码大致可以理解为如下:

枚举右边界,即right++:
	把右边界的值加入当前计算,不同的题计算逻辑不同
	if (满足某条件,左边界需要收缩)
		收缩之前更改记录,即把左边界的值从当前记录中去掉
		左边界收缩,即left++

难的题应该就是计算逻辑复杂一些,判断左边界收缩的条件复杂一些,以及如何看出来这是一道滑动窗口的题,个人的刷题量也比较少,后续有新的理解再补。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值