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(无重复字符的最长子串)一样是滑动窗口的经典题,但有两个不同:
- LC3 求最长,这道题求最短
- 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,说明当前窗口是一个合法答案。此时:
- 先记录当前窗口长度
right - left + 1,和min_len取较小值 - 然后把左边界的元素从窗口和中减去,左指针右移一步
- 继续检查收缩后的窗口是否仍然合法——如果还是
>= 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 一样,虽然代码里有 for 套 while,但:
right指针从头到尾走了 n 步left指针也是单向右移,总共最多走 n 步
两个指针加起来总共最多走 2n 步,所以时间复杂度是 O(n)。
空间复杂度:O(1)
只用了三个变量(left、window_sum、min_len),没有使用额外的数据结构,空间复杂度是常数级别。
七、总结
| 要点 | 内容 |
|---|---|
| 核心思想 | 滑动窗口,右扩左缩找最短合法子数组 |
| 关键前提 | 所有元素为正整数 → 窗口和具有单调性 |
| 扩张条件 | 窗口和 < target,右指针右移 |
| 收缩条件 | 窗口和 >= target,左指针右移并更新最短长度 |
| 时间复杂度 | O(n),双指针各走一遍 |
| 空间复杂度 | O(1),只用常数变量 |
这道题和 LC3(无重复字符的最长子串)是滑动窗口的一对经典组合,值得放在一起对比:
| LC3 无重复最长子串 | LC209 长度最小的子数组 | |
|---|---|---|
| 目标 | 求最长合法窗口 | 求最短合法窗口 |
| 收缩时机 | 窗口不合法时收缩 | 窗口合法时收缩 |
| 更新答案时机 | 每次窗口合法时更新 max | 每次准备收缩前更新 min |
| 窗口维护工具 | 集合(判重) | 累加和(判总和) |
掌握了这两道题,滑动窗口的两种基本模式就都见过了:"不合法就缩"找最长,和"合法就缩"找最短。后面遇到更复杂的窗口题(比如 LC76 最小覆盖子串),只需要换掉"合法条件"的定义,框架是一样的。

浙公网安备 33010602011771号