LeetCode 209:长度最小的子数组 ——「滑动窗口」的典范应用

180 阅读4分钟

在算法面试和刷题过程中,子数组和区间问题是高频考点。今天我们聚焦一道经典题目——长度最小的子数组(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

题目链接: leetcode 209.题目链接


二、题目分析

1. 题意拆解

  • 连续子数组:必须是数组中一段连续的元素。
  • 和 ≥ s:子数组所有元素之和要大于等于目标值 s。
  • 最小长度:在所有满足条件的子数组中,返回长度最小的那一个。

2. 暴力解法的瓶颈

最直接的思路是:枚举所有可能的子数组,计算每个子数组的和,判断是否满足条件,记录最小长度。

  • 时间复杂度:O(n²)
  • 空间复杂度:O(1)

对于 n 达到 10^5 的数据规模,暴力法会超时,必须优化。


三、滑动窗口思想

1. 什么是滑动窗口?

滑动窗口是一种高效处理区间问题的技巧。它通过维护一个“窗口”区间,不断调整窗口的左右边界,动态维护区间内的状态(如和、最大/最小值等),从而避免重复计算。

2. 本题滑动窗口的核心思路

  • 用两个指针 leftright 表示窗口的左右边界。
  • right 向右扩展窗口,累加窗口内的和 sum
  • 每当 sum ≥ target 时,尝试收缩左边界 left,以获得更短的子数组,并更新最小长度。
  • 不断重复,直到遍历完整个数组。

四、代码实现与详细注释

下面是标准的滑动窗口解法:

function minSubArrayLen(target, nums) {
    let len = nums.length;
    let minLen = Infinity; // 初始化为无穷大
    let sum = 0;           // 当前窗口的和
    let left = 0;         // 左边界
    let right = 0;        // 有边界

   while(right < len) {
        sum += nums[right];  // 扩展右边界

        // 当窗口内的和大于等于 target 时,尝试收缩左边界
        while (sum >= target) {
            minLen = Math.min(minLen, right - left + 1); // 更新最小长度
            sum -= nums[left]; // 收缩左边界
            left++;
        }
        right++
    }

    // 如果没有找到符合条件的子数组,返回0
    return minLen === Infinity ? 0 : minLen;
}

代码详解

  • minLen 用于记录当前找到的最小长度,初始为 Infinity,这样便于后续比较。
  • 外层 while 循环移动右边界 right ,逐步扩展窗口
  • 内层 while 循环在满足条件时,尽可能收缩左边界,寻找更短的子数组。
  • 每次收缩窗口时,更新 minLen
  • 最后判断 minLen 是否被更新过,若未更新说明不存在符合条件的子数组,返回0。

五、滑动窗口的执行过程举例

以示例 target = 7, nums = [2,3,1,2,4,3] 为例,滑动窗口的变化如下:

  1. right=0,sum=2,不满足
  2. right=1,sum=5,不满足
  3. right=2,sum=6,不满足
  4. right=3,sum=8,满足,窗口长度4,尝试收缩
    • 收缩后 sum=6,不满足
  5. right=4,sum=10,满足,窗口长度3,继续收缩
    • 收缩后 sum=7,窗口长度2,继续收缩
    • 收缩后 sum=4,不满足
  6. right=5,sum=7,满足,窗口长度2,继续收缩
    • 收缩后 sum=3,不满足

最终,最小长度为2。


六、常见问题与细节

1. 为什么 minLen 初始值用 Infinity

如果用0,Math.min(0, x) 永远是0,无法正确更新。用 Infinity 可以确保第一次找到满足条件的子数组时,minLen 会被正确赋值。

2. 为什么要用 while 循环收缩左边界?

因为每次右边界扩展后,可能窗口内的和远大于 target,需要多次收缩左边界,才能找到最短的子数组。

3. 时间复杂度分析

  • 每个元素最多被访问两次(一次右扩、一次左缩),总复杂度 O(n),非常高效。

七、总结

  • 本题是滑动窗口思想的典型应用,适合初学者练习。
  • 通过维护窗口的左右边界,动态调整区间,极大提升效率。
  • 代码简洁,易于理解,适合面试和刷题时快速实现。

八、完整代码

function minSubArrayLen(target, nums) {
    let len = nums.length;
    let minLen = Infinity; 
    let sum = 0;           
    let left = 0;        
    let right = 0;        

   while(right < len) {
        sum += nums[right];  
        while (sum >= target) {
            minLen = Math.min(minLen, right - left + 1);
            sum -= nums[left]; 界
            left++;
        }
        right++
    }
    return minLen === Infinity ? 0 : minLen;
}

九、结语

滑动窗口是解决区间类问题的利器。通过本题的练习,你不仅能掌握滑动窗口的基本用法,还能体会到算法优化的乐趣。希望你能举一反三,将这一技巧应用到更多类似问题中!