算法通关村第十六关——滑动窗口其实很简单(青铜)

1. 滑动窗口基本思想

1.1 基本概念

在数组双指针里,我们介绍过“对撞型”和“快慢型”两种方式,而滑动窗口思想其实就是快慢型的特例。学过计算机网络的同学都知道滑动窗口协议(Sliding Window Protocol),该协议是TCP实现流量控制等的核心策略之一。事实上在与流量控制、熔断、限流、超时等场景下都会首先从滑动窗口的角度来思考问题,例如hystrix、sentinel等框架都使用了这种思想。

滑动窗口是一种常用的算法技巧,用于解决字符串或数组相关的问题。它的基本思想是维护一个固定大小的窗口,通过移动这个窗口来寻找满足特定条件的子串或子数组。

为了更好地理解滑动窗口的基本思想,我们可以通过一个生活中的例子来进行讲解。

例子:

假设你在一个盛宴上,想要寻找最长的连续饮料供应区间,使得这个区间内的每种饮料都不重复。你可以使用滑动窗口的方法来解决这个问题。

  1. 首先,我们定义一个窗口,初始时窗口为空。然后,我们从第一个饮料开始遍历,并将它加入到窗口中。
  2. 此时,窗口内只有这一种饮料。
  3. 接下来,我们不断向右移动窗口的右边界,并检查新加入的饮料是否与窗口内已有的饮料重复。
  4. 如果重复了,就需要移动窗口的左边界,直到窗口内没有重复的饮料为止。
  5. 这样,我们就找到了一个满足条件的窗口。
  6. 然后,我们记录下当前窗口的长度,并更新最长连续饮料供应区间的长度。

通过不断重复上述过程,我们就可以找到最长的连续饮料供应区间。这个例子展示了滑动窗口的基本思想,即通过移动窗口的左右边界来寻找满足特定条件的子串或子数组。

下面是一个简单的示意图,说明了滑动窗口的一步步过程:

初始状态:
[ ] (空窗口)

第一步:加入第一种饮料
[A] B C D E F

第二步:加入第二种饮料
[A, B] C D E F

第三步:加入第三种饮料
[A, B, C] D E F
此时窗口内没有重复的饮料,记录当前窗口长度为3。

第四步:加入第四种饮料
[A, B, C, D] E F
此时窗口内没有重复的饮料,记录当前窗口长度为4。

第五步:加入第五种饮料
[A, B, C, D, E] F
此时窗口内有重复的饮料(与第三种饮料C重复),需要移动窗口的左边界。

第六步:移动窗口的左边界
A [B, C, D, E] F
此时窗口内没有重复的饮料,记录当前窗口长度为4。

继续上述步骤,直到遍历完所有的饮料。最后,我们就可以得到最长的连续饮料供应区间的长度。

总结起来,滑动窗口是一种通过移动窗口的边界来寻找满足条件的子串或子数组的算法技巧。它能够在O(n)的时间复杂度内解决一些字符串或数组相关的问题,具有较高的效率和性能。

1.2 主要的两种类型

这个例子已经告诉我们了什么是窗口、什么是窗口的滑动:

**窗口:**窗口其实就是两个变量left和right之间的元素,也可以理解为一个区间。窗口大小可能固定,也可能变化,如果是固定大小的,那么自然要先确定窗口是否越界,再执行逻辑处理。如果不是固定的,就要先判断是否满足要求,再执行逻辑处理。

**滑动:**说明这个窗口是移动的,事实上移动的仍然是left和right两个变量,而不是序列中的元素。当变量移动的时,其中间的元素必然会发生变化,因此就有了这种不断滑动的效果。

在实际问题中,窗口大小不一定是固定的,我们可以思考两种场景:

  1. 固定窗口的滑动就是火车行驶这种大小不变的移动 。

  2. 可变的窗口就像两个老师带着一队学生外出,一个负责开路,一个负责断后,中间则是小朋友。两位老师之间的距离可能有时大有时小,但是整体窗口是不断滑动的。

根据窗口大小是否固定,可以造出两种类型的题:

  1. 如果是固定的,则一般会让你求哪个窗口的元素最大、最小、平均值、和最大、和最小等等类型的问题。

  2. 如果窗口是变的,则一般会让你求一个序列里最大、最小窗口是什么等等。

滑动窗口题目本身没有太高的思维含量,但是实际在解题的时候仍然会感觉比较吃力,主要原因有以下几点:

  1. 解题最终要落实到数组上,特别是边界处理上,这是容易晕的地方,稍有疏忽就难以得到准确的结果。

  2. 有些元素的比较、判断等比较麻烦,不仅要借助集合等工具,而且处理过程中还有一些技巧,如果不熟悉会导致解题难度非常大。

  3. 堆!我们在前面介绍过,堆结构非常适合在流数据中找固定区间内的最大、最小等问题。因此滑动窗口经常和堆一起使用可以完美解决很多复杂的问题。

最后一个问题,那双指针和滑动窗口啥区别呢?根据性质我们可以看到,滑动窗口是双指针的一种类型,主要关注两个指针之间元素的情况,因此范围更小一些,而双指针的应用范围更大,花样也更多。

1.3 常用于解决的问题

滑动窗口常用于解决字符串或数组相关的问题,特别是以下几类问题:

  1. 求取连续子数组或子串的最大/最小值(和、乘积等):例如,求一个数组中长度为k的连续子数组的最大和/最小和。
  2. 寻找满足特定条件的子数组或子串:例如,寻找一个数组中和大于等于某个值的最短连续子数组。
  3. 判断是否存在满足特定条件的子数组或子串:例如,判断一个字符串中是否存在包含某些字符的连续子串。
  4. 计算满足特定条件的子数组或子串的个数:例如,计算一个数组中所有和等于某个值的连续子数组的个数。
  5. 通过窗口移动来更新结果:例如,动态维护一个滑动窗口内的最大/最小值。

以下是一些常见的问题和对应的力扣题目,其解决方案使用了滑动窗口算法:

  1. 无重复字符的最长子串:给定一个字符串,请找出其中不含有重复字符的最长子串的长度。

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

  2. 替换后的最长重复字符:给定一个只包含大写英文字母的字符串,你可以将其中任意位置的字符替换成任意其他大写英文字母,使得最终的字符串中所包含的连续相同字符的最长长度最大。返回替换后的最长重复字符的长度。

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

  3. 最小覆盖子串:给定一个字符串 S 和一个字符串 T,请在 S 中找出包含 T 所有字母的最小子串。

    题目:76. 最小覆盖子串

  4. 和相同的二元子数组:给你一个二元数组 nums ,和一个整数 goal ,请你统计并返回有多少个和为 goal非空 子数组。

    题目:930. 和相同的二元子数组

  5. 数组中的最长山脉:给定一个整数数组 A,返回最长的山脉的长度。

    题目:845. 数组中的最长山脉

这些题目都可以通过使用滑动窗口算法来解决,具体的实现和算法细节可以参考对应题目的解答。

2 两个入门题

2.1 子数组最大平均数

leetcode 643. 子数组最大平均数 I

比较经典和简单题:

  1. left和right距离为k的时候才需要计算值
  2. right往右移动,距离不到k的时候,只需要计算合
  3. 达到距离k为的时候,计算出avg,然后sum-=num[left]和left++;
class Solution {
    public double findMaxAverage(int[] nums, int k) {
        int left = 0; 
        int sum = 0;
        double avg = Double.NEGATIVE_INFINITY; // 初始化为负无穷大
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            
            if (right - left + 1 == k) { // 窗口大小达到k
                avg = Math.max(avg, (double) sum / k);
                sum -= nums[left];
                left++;
            }
        }
        return avg;
    }
}

不过可以优化一下,下面这样会快一点:

class Solution {
    public double findMaxAverage(int[] nums, int k) {
        int sum = 0;
        if (k > nums.length || nums.length < 1 || k < 1) {
            return 0;
        }
        for (int i = 0; i < k; i++) {
            sum += nums[i];
        }
        
        int maxAvg = sum;
        
        for (int i = k; i < nums.length; i++) {
            sum += nums[i] - nums[i - k];
            maxAvg = Math.max(maxAvg, sum);
        }
        
        return (double) maxAvg / k;
    }
}

2.2 最长连续递增序列

leetcode 674. 最长连续递增序列

这道题跟上面那题大差不差,也是比较简单,只是滑动的时候,left不太一样

  • 当nums[right] < nums[right-1]时,就移动right,否则left = right
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        if (nums.length <= 1) {
            return nums.length == 1 ? 1 : 0;
        }
        int res = 1;
        int left = 0;
        int len = 0;
        for(int right = 1; right < nums.length; right++){
            if(nums[right] > nums[right-1]){
                len = right - left + 1;
                res = Math.max(res, len);
            }else{
                left = right;
            }
        }
        return res;
    }
}

over,入门完毕~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值