【滑动窗口专题】第一讲:长度最小的子数组
🌿 伸缩窗口,快速捕捉满足条件的最短区间,滑动窗口的精髓由此揭开序幕。
🌱 文章摘要
本篇作为「滑动窗口专题」的开篇,带你从一道经典的数组题出发,理解滑动窗口的核心思想:通过左右指针的“扩张—收缩”操作,在 O(n) 的时间复杂度内快速捕捉满足条件的最短区间。这道题是后续多种字符串和数组窗口题的基础。
🌊 在正式开启滑动窗口之前,如果你还没看过我的《双指针专题》
👉双指针专题_明天会有多晴朗的博客-CSDN博客,强烈推荐从那里起航 🚀
那里从最基础的「移动零」到进阶的「四数之和」,能帮助你更顺畅地理解窗口技巧的演变过程~
🧭 导读
在“双指针专题”中,我们熟悉了“固定+移动”的技巧。而进入“滑动窗口专题”后,问题的核心从“数值对撞”转变为“区间伸缩”。
本篇,我们将从一道非常经典的题目——长度最小的子数组(Minimum Size Subarray Sum)入手,掌握「何时扩张窗口,何时收缩窗口」的基本套路,为后续的《无重复字符的最长子串》《最小覆盖子串》等打下坚实的基础。
一、题目描述
题目:
给定一个含正整数的数组nums
和一个正整数target
,找出该数组中满足 和 ≥ target 的最短连续子数组,并返回其长度;如果不存在,返回 0。示例:
输入:target = 7, nums = [2,3,1,2,4,3] 输出:2 解释:子数组 [4,3] 的和 ≥ 7,且长度最短。
二、思路引导:滑动窗口的“扩张—收缩”
1. 直觉与关键观察
题目要求连续子数组且数组元素为正数。这两个条件非常关键:连续保证我们可以用两个指针表示一个窗口;正数保证当我们把窗口右端向右移动时,窗口和只会单调不减,把左端向右移动时窗口和只会单调不增。
因此,维护一个
[left, right]
的窗口,右端向右扩张来尝试“满足条件”,一旦满足就尽量收缩左端以尝试得到更短区间——这是一个贪心且不会错过最优解的策略。
2. 为什么“右扩后左缩”不会漏解?
- 设我们固定某次扩到
right = r
,此时窗口[L, r]
首次满足sum ≥ target
(含首次情况或后续某次)。- 因为所有元素为正,若我们在
L
之前有更大的L' < L
使[L', r]
满足条件,那么当右指针第一次到达r
时,[L', r]
也会已被考虑过(因为左指针只会向右移动)。- 反过来也成立:当窗口满足条件并尝试缩左时,任何缩左后依旧满足条件的更短子区间都会被检测并更新答案。
- 因此该策略不会漏掉任何可能的最短窗口。
3. 与暴力法的对比
- 暴力枚举所有
(i, j)
对计算和 → O(n²)(或更差),不够高效。- 滑动窗口用单次线性扫描,左/右指针合计移动次数 ≤ 2n → O(n),在数组元素为正时是最优的常用方案。
三、窗口变化示意
数组:[2, 3, 1, 2, 4, 3]
,target = 7
Step 1: right = 0, sum = 2 < 7 → 右移 right
[2] 3 1 2 4 3^Step 2: right = 1, sum = 5 < 7 → 继续右移
[2 3] 1 2 4 3^ ^Step 3: right = 2, sum = 6 < 7 → 继续右移
[2 3 1] 2 4 3^ ^Step 4: right = 3, sum = 8 ≥ 7 √ → 记录长度 4
→ 收缩 left,sum=8-2=62 [3 1 2] 4 3^ ^Step 5: right = 4, sum = 10 ≥ 7 → 记录长度 3
→ 收缩 left,sum=10-3=72 3 [1 2 4] 3^ ^Step 6: right = 4, sum = 7 ≥ 7 → 记录长度 2
→ 收缩 left,sum=7-1=6 ×Step 7: right = 5, sum = 6+3=9 ≥ 7 → 更新长度
→ 收缩 left,找到最短长度 2([4,3])
核心:每个元素进窗口一次、出窗口一次,总复杂度 O(n)。
四、代码实现(C++)
#include <vector>
#include <climits>
using namespace std;class Solution {
public:int minSubArrayLen(int target, vector<int>& nums) {int n = nums.size();int left = 0, sum = 0, ans = INT_MAX;for (int right = 0; right < n; ++right) {sum += nums[right];// 每次扩张后,尽量收缩左边以寻找更短区间while (sum >= target) {ans = min(ans, right - left + 1);sum -= nums[left++];}}return ans == INT_MAX ? 0 : ans;}
};
五、代码剖析与细节问题
1. 每个指针的移动次数为何是 O(n)
右指针
right
在主循环中最多移动n
次(从 0 到 n-1)。左指针
left
在内部收缩循环中也至多移动n
次(每次收缩都使 left 增加),left
永远不会回退。
→ 因此总指针移动次数 ≤ 2n,算法为 O(n)。
2. 为什么要求数组中的元素为正数?
若元素均为正数,窗口扩张会使
sum
单调递增,收缩会使sum
单调递减;这使得我们可以用两指针做贪心判断并保证正确性。若存在负数,收缩可能导致
sum
增大或扩张导致sum
减小,单调性被破坏,滑动窗口策略需要调整。
3. 关于溢出与数据类型
若
nums
元素与target
较大(比如题目允许 10^9),sum
可能溢出int
。在这种极端场景下,应使用long long sum
。在多数题目约束下
int
足够,但写通用代码时用long long
更保险。
4. 常见错误
把
right++
放在错误位置导致漏算当前窗口;推荐不要把right++
放在收缩循环内部。忘记处理
ans
未更新的情况(返回0
)。使用
INT_MAX
然后做ans + 1
等操作可能溢出,推荐比较并在最后判断。当需要输出索引时,务必在更新
ans
前记录left
/right
的当前值,否则left
会在收缩后改变。
六、复杂度分析
-
时间复杂度
O(n):
right
从 0 增到n-1
;left
从 0 也最多增到n-1
。每个元素最多进窗口一次、出窗口一次。所谓“摊还分析” (amortized analysis):虽然有嵌套的while
,但内部while
的总执行次数不会超过n
,因此总复杂度线性。
-
空间复杂度
O(1):使用固定数量的整数变量
left/right/sum/ans
等。若你还要返回子数组内容或索引,额外开销为 O(1)(只是几个索引)或 O(k)(若复制子数组内容则为其长度 k)。
七、总结
- 本题是滑动窗口的入门经典:熟练掌握「右扩→左缩」的节奏与“单调性”前提(元素为正)是关键。
- 代码实现简洁,但细节(收缩时机、边界处理、类型溢出、返回值约定)会决定你是否通过面试题与线上判题。
- 掌握后,向字符串类和更复杂的“窗口状态维护”题(字符计数、频率表、单调队列)顺利过渡将变得非常自然。
🔜 下一讲预告
下一篇我们将进入《无重复字符的最长子串》,这是一道经典的字符串型滑动窗口题。
- 窗口如何动态维护字符出现次数?
- 如何在扩张和收缩之间平衡窗口长度?
下一讲我们将逐步拆解并给出高效实现 !
📚 系列更新提示
本文是《滑动窗口专题》的第 1 篇。
接下来我们将从「最短子数组」出发,逐步攻克字符串、数组中的各种滑动窗口题目,让你真正掌握“窗口的伸缩与状态维护”这项算法利器。
📌 系列持续更新中,欢迎 点赞、收藏、关注,不要错过每一次窗口技巧的进阶!🚀