LeetCode【刷题日记】:滑动窗口算法详解:从暴力法到最优解

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:今天忙着写学校的作业,只能简单的写两个算法题了,其实算法题也不简单,对于我这种小白来说,今天依旧是数组篇,给大家推荐我最近在读**《数据结构与算法》**这本书,我个人感觉讲的挺好的,适合新手入门。

一 最小长度子数组

题目背景:LeetCode 209

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。

示例:

  • 输入:s = 7, nums = [2,3,1,2,4,3]
  • 输出:2
  • 解释:子数组 [4,3] 是该条件下的长度最小的子数组。

提示:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5

题目分析 :这个题目,我们一拿到手想到的肯定是最暴力的双层for循环,通过内外嵌套的循环来枚举所有可能的子数组,找到第一个满足条件的,然后返回最短的,进行求和比对

但是缺点也很明显,时间复杂度高 - O(n²),需要 大量重复计算,空间复杂度虽然低,但时间代价太大。由上一节我们学习的双指针法在这也能派上用场,有关数组的查询问题,双指针是不错的方法,在这里也叫做滑动窗口法,比较形象,本质都是一样,下面我们具体解释。

解法一:暴力拆解

java 复制代码
int result = Integer.MAX_VALUE;  // 记录最短长度
for (int i = 0; i < nums.size(); i++) {      // 外层:确定子数组起点
    int sum = 0;                              // 重置和
    for (int j = i; j < nums.size(); j++) {  // 内层:确定子数组终点
        sum += nums[j];                       // 累加
        if (sum >= s) {                       // 找到第一个满足条件的
            int subLength = j - i + 1;        // 计算长度
            result = Math.min(result, subLength); // 更新最短长度
            break;                            // 这个起点找到了,继续下一个起点
        }
    }
}

具体实现的流程:

逐步演示

s = 7, nums = [2,3,1,2,4,3] 为例:

第1轮:起点 i=0
复制代码
j=0: sum=2, 2<7 → 继续
j=1: sum=2+3=5, 5<7 → 继续
j=2: sum=5+1=6, 6<7 → 继续
j=3: sum=6+2=8, 8≥7 → 满足!
     子数组[2,3,1,2],长度=4
     result = min(∞,4) = 4
     break (结束内层循环)

找到起点0的第一个满足条件的子数组:[2,3,1,2]

第2轮:起点 i=1
复制代码
j=1: sum=3, 3<7 → 继续
j=2: sum=3+1=4, 4<7 → 继续
j=3: sum=4+2=6, 6<7 → 继续
j=4: sum=6+4=10, 10≥7 → 满足!
     子数组[3,1,2,4],长度=4
     result = min(4,4) = 4
     break
第3轮:起点 i=2
复制代码
j=2: sum=1, 1<7 → 继续
j=3: sum=1+2=3, 3<7 → 继续
j=4: sum=3+4=7, 7≥7 → 满足!
     子数组[1,2,4],长度=3
     result = min(4,3) = 3  ← 更新为更短的
     break
第4轮:起点 i=3
复制代码
j=3: sum=2, 2<7 → 继续
j=4: sum=2+4=6, 6<7 → 继续
j=5: sum=6+3=9, 9≥7 → 满足!
     子数组[2,4,3],长度=3
     result = min(3,3) = 3
     break
第5轮:起点 i=4
复制代码
j=4: sum=4, 4<7 → 继续
j=5: sum=4+3=7, 7≥7 → 满足!
     子数组[4,3],长度=2
     result = min(3,2) = 2  ← 更新为更短的
     break
第6轮:起点 i=5
复制代码
j=5: sum=3, 3<7 → 无法满足
     内层循环结束,没有break
最终结果
复制代码
result = 2

解法二:滑动窗口法

java 复制代码
class Solution {

    // 滑动窗口
    public int minSubArrayLen(int s, int[] nums) {
        int left = 0;
        int sum = 0;
        int result = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            while (sum >= s) {
                result = Math.min(result, right - left + 1);
                sum -= nums[left++];
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}

具体解析:

什么是滑动窗口法呢,实际就是双指针,我们定义了两个可以自行移动的指针(移动的逻辑由我们控制),在这里,我们要先明确,在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。那么滑动窗口如何用一个for循环来完成这个操作呢。

我们在这个方法中只用到了一个for循环,问题来了,我们用for循环控制的是起始索引还是终止索引呢,如果是起始索引,那么从逻辑上来说跟第一种的暴力解法一致,因此显而易见,我们用for循环来控制终止索引

如图所示,我们通过for循环来控制 j,也就是终止位置

而代码部分的whlie循环,以这个终止位置为前提条件,进行判断和数组求和比对,如果不满足题目所要的子数组和大于目标值,就不执行for循环内部的while循环,这样就移动数组的终止位置,然后再进行while循环

当满足时,也就是子数组的和大于等于目标值,我们调用Math函数来对所要返回的result进行取小(题目要求的),后续的逻辑就是,既然我们已经满足了题目的第一个条件(大于等于目标值),那么我们就要处理第二个条件(长度最小),因此我们小缩小起始位置,如上面的动图,这也是while循环所做的判断,left++,这便是滑动窗口的精髓所在!滑动窗口的精髓就在于窗口的右边界不断右移,左边界通过 while 循环动态调整,从而在 O(n) 时间内找到全局最优解。

关于条件判断,我们为什么用while语句而不用if,道理很简单,但是很容易顺手写错,因为我们不知道要缩进几次才是最小的,while可以根据条件执行多次,只要是符合的,而if只根据条件执行一次。

更专业的说,

  • while:持续收缩,找到以当前 right 为结尾的最优解

  • if:只收缩一次,可能错过更优解

相关题目拓展:

题目背景:LeetCode 904

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

示例 1:

复制代码
输入:fruits = [1,2,1]
输出:3
解释:可以采摘全部 3 棵树。

示例 2:

复制代码
输入:fruits = [0,1,2,2]
输出:3
解释:可以采摘 [1,2,2] 这三棵树。
如果从第一棵树开始采摘,则只能采摘 [0,1] 这两棵树。

示例 3:

复制代码
输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

示例 4:

复制代码
输入:fruits = [3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:可以采摘 [1,2,1,1,2] 这五棵树。

题目解析:

我们拿到题目一定要认真的解读一下示例,这些示例都是很有代表性的,而不是自己随意去揣测题目的背景,很容易把自己陷进去,作者本人就是,在写这题的时候,我在想每颗树上的水果都不一样,弄来弄去不就是能摘两种类型的水果吗,一直想不明白,后来才发现树上的水果不确定,是任意的,其实这在示例中就提醒我们了:

题目给的例子就体现了随机性:

示例1[1,2,1] - 有重复
示例2[0,1,2,2] - 有连续重复
示例3[1,2,3,2,2] - 混合分布
示例4[3,3,3,1,2,1,1,2,3,3,4] - 复杂随机分布

java 复制代码
class Solution {
    public int totalFruit(int[] fruits) {
        Map<Integer, Integer> basket = new HashMap<>();
        int left = 0;
        int maxCount = 0;
        for(int right=0;right<fruits.length;right++){
            basket.put(fruits[right],basket.getOrDefault(fruits[right],0)+1);

             while (basket.size() > 2) {
                basket.put(fruits[left], basket.get(fruits[left]) - 1);
                if (basket.get(fruits[left]) == 0) {
                    basket.remove(fruits[left]);
                }
                left++;
            }
              // 更新最大长度
            maxCount = Math.max(maxCount, right - left + 1);
        }
          
        
        return maxCount;
    }
}

题目讲解:

这道题的原理跟我们上面的那道题一样,只不过从数组变成了集合,目标是找到最长的连续子数组,其中最多包含两种不同的水果(元素),使用两个指针 leftright 维护一个窗口:

right 向右移动,扩大窗口(采摘新水果)

当窗口内水果种类超过2种时,移动 left 缩小窗口(放弃最早的水果)每次窗口调整后,记录当前窗口长度,取最大值

把当前水果加入窗口,并更新计数。

basket.put(fruits[right], basket.getOrDefault(fruits[right], 0) + 1);

getOrDefault 的作用

如果这种水果已经在篮子里,获取当前数量

如果不在篮子里,返回默认值0

然后+1,表示又摘了一个

收缩过程

  1. 获取左边界指向的水果种类:fruits[left]

  2. 将这种水果的数量减1(因为要移除一棵树)

  3. 如果数量变成0,从map中删除这个键(表示这种水果完全移出窗口)

  4. left++,左边界右移一位

  5. 重复检查,直到种类数 ≤ 2

重要maxCount 的更新是在 while 循环之后,不是在之前

总结

问题 答案
收缩后的长度小于之前记录的长度怎么办? 没关系,我们保留的是历史最大值
会不会丢失更长的合法窗口? 不会,因为更长且合法的窗口在后续 right 移动中会被记录
为什么不先记录再收缩? 因为可能记录到不满足条件的窗口

核心思想 :滑动窗口保证每次记录的都是当前 right 下,满足条件的最长窗口 ,而全局最大值通过 Math.max() 不断更新,不会丢失历史的最佳结果。

结语:如果对你有帮助,请点赞,关注,收藏,你的鼓励就是我最大的支持!

相关推荐
凤年徐2 小时前
封装红黑树实现 mymap 和 myset
网络·c++·算法
迷藏4942 小时前
# 发散创新:用Locust实现高并发场景下的精准压力测试实战在现代微服务架构中,**系统稳定性与性能瓶颈的识别能力直接决定了产品上线后
java·python·微服务·架构·压力测试
秃头狂魔2 小时前
【HOT100】DAY1
算法·哈希算法
MicroTech20252 小时前
MLGO微算法科技分布式量子算法模拟技术:以动态量子电路推动可扩展量子计算
科技·算法·量子计算
这辈子谁会真的心疼你2 小时前
怎么修改视频的拍摄信息?详细的修改过程
java·服务器·音视频
实名上网宋凯宣2 小时前
水电参与电力市场研究(2)_内含代码
算法·电力市场
小碗羊肉2 小时前
【从零开始学Java | 第二十四篇】泛型的继承和通配符
java·开发语言·新手入门
不知名的老吴2 小时前
“程序 = 算法 + 数据结构”的拓展与启示
算法
阿i索2 小时前
【蓝桥杯备赛Day4】基础算法
笔记·算法·蓝桥杯