LeetCode-209:长度最小的子数组,滑动窗口的进阶——右扩左缩找最短合法区间

本题在线练习LeetCode 209. 长度最小的子数组 — 在线练习(免费 · 无需登录 · AI 辅助)

配套刷题网站 Zero2Leetcode —— 内置本地 OJ + AI 教练,零门槛开刷 Hot 100。

一、题目概述

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

示例 1

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

示例 2

输入:target = 4, nums = [1,4,4]
输出:1
解释:子数组 [4] 满足条件,长度为 1。

示例 3

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
解释:所有元素之和为 8,不存在满足条件的子数组。

二、核心思路

暴力做法:枚举所有子数组 O(n^2)

最直接的想法是:枚举每一个起点 i,然后从 i 开始累加,找到第一个满足 sum >= target 的位置 j,记录长度 j - i + 1。所有起点中取最短的。

这样做时间复杂度是 O(n^2),当数据量大时会超时。

滑动窗口:O(n) 最优解

这道题和 LC3(无重复字符的最长子串)一样是滑动窗口的经典题,但有两个不同:

  1. LC3 求最长,这道题求最短
  2. LC3 的收缩条件是"窗口不合法时收缩",这道题的收缩条件是"窗口已经合法了,尝试收缩看能不能更短"

核心逻辑用一句话概括:

右指针不断扩张使窗口和达标,达标后不断收缩左指针寻找最短长度。

为什么滑动窗口在这道题上是正确的?

关键在于题目的一个条件:数组中所有元素都是正整数。这意味着:

  • 右指针右移 → 窗口和一定增大
  • 左指针右移 → 窗口和一定减小

窗口和关于窗口大小具有单调性。正因为有这个单调性,当窗口和已经 >= target 时,右指针没有必要继续扩张(只会让窗口更长),应该尝试收缩左指针来找更短的答案。反过来说,如果数组中有负数,扩大窗口不一定增大和,收缩窗口不一定减小和,滑动窗口就不适用了。


三、代码实现

class Solution:
    def minSubArrayLen(self, target: int, nums: list[int]) -> int:
        left = 0
        window_sum = 0
        min_len = float('inf')

        for right in range(len(nums)):
            window_sum += nums[right]
            while window_sum >= target:
                min_len = min(min_len, right - left + 1)
                window_sum -= nums[left]
                left += 1

        return min_len if min_len != float('inf') else 0

四、逐行拆解

1. 初始化三个变量

left = 0
window_sum = 0
min_len = float('inf')
  • left:窗口的左边界,初始指向数组开头。
  • window_sum:当前窗口内所有元素的和。
  • min_len:记录满足条件的最短子数组长度。初始化为正无穷,方便后续取 min。如果整个过程中从未更新过,说明不存在合法子数组,最终返回 0。

2. 右指针遍历整个数组

for right in range(len(nums)):
    window_sum += nums[right]

right 是窗口的右边界。每次右移一步,把新元素加入窗口和。


3. 窗口和达标时,收缩左边界

while window_sum >= target:
    min_len = min(min_len, right - left + 1)
    window_sum -= nums[left]
    left += 1

这是整个算法最关键的部分。

当窗口和已经 >= target,说明当前窗口是一个合法答案。此时:

  1. 先记录当前窗口长度 right - left + 1,和 min_len 取较小值
  2. 然后把左边界的元素从窗口和中减去,左指针右移一步
  3. 继续检查收缩后的窗口是否仍然合法——如果还是 >= target,继续收缩

注意这里用 while 而不是 if。因为收缩一步后窗口可能仍然满足条件,需要一直缩到不满足为止,才能保证找到以当前 right 为右端点的最短合法子数组。


4. 返回结果

return min_len if min_len != float('inf') else 0

如果 min_len 被更新过,返回它;如果始终是正无穷,说明不存在合法子数组,返回 0。


五、手动模拟

target = 7, nums = [2, 3, 1, 2, 4, 3] 为例,走一遍完整流程。

步骤 right nums[right] window_sum 达标? 操作 left 窗口内容 min_len
1 0 2 2 继续扩张 0 [2] inf
2 1 3 5 继续扩张 0 [2,3] inf
3 2 1 6 继续扩张 0 [2,3,1] inf
4 3 2 8 记录长度4,减去nums[0]=2,left→1 1 [3,1,2] 4
6 停止收缩 1 [3,1,2] 4
5 4 4 10 记录长度4,减去nums[1]=3,left→2 2 [1,2,4] 4
7 记录长度3,减去nums[2]=1,left→3 3 [2,4] 3
6 停止收缩 3 [2,4] 3
6 5 3 9 记录长度3,减去nums[3]=2,left→4 4 [4,3] 3
7 记录长度2,减去nums[4]=4,left→5 5 [3] 2
3 停止收缩 5 [3] 2

最终返回 2,对应最短子数组 [4, 3],总和为 7。

关键观察:在第 6 步中,右指针加入 3 后窗口和为 9,连续收缩两次左指针,窗口从 [2,4,3] 缩到 [4,3],仍然满足 >= 7,此时长度为 2,是最短的。


六、复杂度分析

时间复杂度:O(n)

和 LC3 一样,虽然代码里有 forwhile,但:

  • right 指针从头到尾走了 n 步
  • left 指针也是单向右移,总共最多走 n 步

两个指针加起来总共最多走 2n 步,所以时间复杂度是 O(n)。

空间复杂度:O(1)

只用了三个变量(leftwindow_summin_len),没有使用额外的数据结构,空间复杂度是常数级别。


七、总结

要点 内容
核心思想 滑动窗口,右扩左缩找最短合法子数组
关键前提 所有元素为正整数 → 窗口和具有单调性
扩张条件 窗口和 < target,右指针右移
收缩条件 窗口和 >= target,左指针右移并更新最短长度
时间复杂度 O(n),双指针各走一遍
空间复杂度 O(1),只用常数变量

这道题和 LC3(无重复字符的最长子串)是滑动窗口的一对经典组合,值得放在一起对比:

LC3 无重复最长子串 LC209 长度最小的子数组
目标 最长合法窗口 最短合法窗口
收缩时机 窗口不合法时收缩 窗口合法时收缩
更新答案时机 每次窗口合法时更新 max 每次准备收缩前更新 min
窗口维护工具 集合(判重) 累加和(判总和)

掌握了这两道题,滑动窗口的两种基本模式就都见过了:"不合法就缩"找最长,和"合法就缩"找最短。后面遇到更复杂的窗口题(比如 LC76 最小覆盖子串),只需要换掉"合法条件"的定义,框架是一样的。

posted @ 2026-04-20 00:21  孤飞  阅读(83)  评论(0)    收藏  举报