在 LeetCode 数组类题目中,“长度最小的子数组”是一道经典的中等难度题,核心考察对「滑动窗口」算法的理解与应用。这道题看似可以用暴力破解,但其时间复杂度较高,而滑动窗口能实现线性时间求解,是最优解法。本文将从题目分析、算法原理、代码拆解、注意事项四个维度,完整讲解这道题的解题思路。
一、题目回顾
给定一个含有 n 个正整数的数组和一个正整数 target ,找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 的和为 7,是长度最小的符合条件的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
二、算法选型:为什么用滑动窗口?
首先思考暴力解法:枚举所有可能的子数组,计算其和,判断是否大于等于 target,同时记录最小长度。这种方法的时间复杂度是 O(n²),对于 n 较大的场景(本题约束 n 可达 10⁵),会出现超时问题。
而滑动窗口(双指针)算法能将时间复杂度优化至 O(n),核心原因在于:利用数组中元素均为正整数的特性,窗口的扩张和收缩具有单调性,无需重复计算子数组和。
滑动窗口的核心逻辑:维护一个左、右指针构成的窗口,窗口内的元素和为 sum。通过右指针扩张窗口增大 sum,当 sum 满足条件(≥target)时,尝试左指针收缩窗口,缩小窗口长度,同时更新最小长度,直至 sum 不满足条件,再继续扩张右指针。
三、代码逐行解析
先贴出完整代码(TypeScript 实现),再逐行拆解逻辑:
function minSubArrayLen(target: number, nums: number[]): number {
const nL = nums.length;
if (nL === 0) return 0;
let left = 0;
let right = 0;
let res = nL + 1;
let sum = nums[0];
while (left < nL && right < nL) {
if (sum < target) {
right++;
sum += nums[right];
} else {
res = Math.min(res, right - left + 1);
sum -= nums[left];
left++;
}
}
return res === nL + 1 ? 0 : res;
};
1. 初始化变量
-
nL = nums.length:记录数组长度,避免重复计算。 -
if (nL === 0) return 0:边界处理,空数组直接返回 0。 -
left = 0, right = 0:双指针初始化,均指向数组起始位置,窗口初始为 [0,0]。 -
res = nL + 1:初始化最小长度为数组长度+1(一个不可能达到的最大值),用于后续更新最小值。若最终 res 仍为这个值,说明无符合条件的子数组。 -
sum = nums[0]:初始化窗口和为第一个元素,对应初始窗口 [0,0]。
2. 滑动窗口核心循环
循环条件 left < nL && right < nL:确保双指针均在数组范围内,避免越界。
(1)扩张窗口:sum 不足 target 时
if (sum < target) {
right++;
sum += nums[right];
}
当窗口内元素和小于 target 时,需要扩大窗口范围,右指针右移一位,并将新元素加入 sum。这里要注意:right 先自增,再累加 nums[right],此时窗口变为 [left, right]。
(2)收缩窗口:sum 满足条件时
else {
res = Math.min(res, right - left + 1);
sum -= nums[left];
left++;
}
当 sum ≥ target 时,说明当前窗口是一个符合条件的子数组,需要:
-
更新最小长度 res:用当前窗口长度(right - left + 1)与现有 res 取最小值。
-
收缩窗口:左指针右移一位,同时将移出窗口的元素从 sum 中减去,尝试寻找更短的符合条件的子数组。
3. 结果返回
return res === nL + 1 ? 0 : res:若 res 仍为初始值(nL+1),说明没有找到符合条件的子数组,返回 0;否则返回最小长度 res。
四、注意事项与优化点
1. 边界情况处理
-
数组长度为 0:直接返回 0(已处理)。
-
单个元素等于 target:此时窗口长度为 1,直接返回 1(代码可正常处理,因为 sum = nums[0] = target,进入 else 分支,res 更新为 1)。
-
所有元素和小于 target:最终 res 仍为 nL+1,返回 0(符合示例 3 场景)。
2. 算法优化点
本题代码已是滑动窗口的最优实现,但有一个细节可优化:初始化 sum 时,可避免直接赋值 nums[0],而是在循环内累加,让逻辑更统一。优化后代码如下:
function minSubArrayLen(target: number, nums: number[]): number {
const n = nums.length;
let left = 0, sum = 0, res = n + 1;
for (let right = 0; right < n; right++) {
sum += nums[right];
while (sum >= target) {
res = Math.min(res, right - left + 1);
sum -= nums[left++];
}
}
return res > n ? 0 : res;
}
这种实现用 for 循环控制右指针扩张,while 循环控制左指针收缩,逻辑更简洁,避免了原代码中 right 越界累加 undefined 的潜在风险(原代码中当 right 达到 n-1 时,right++ 后 nums[right] 为 undefined,sum 会变成 NaN,但循环条件会终止,不影响结果,不过优化后更严谨)。
3. 为什么元素为正整数是关键?
滑动窗口的单调性依赖于数组元素为正整数:当左指针收缩时,sum 一定会减小;当右指针扩张时,sum 一定会增大。若存在负数,sum 的变化不具备单调性,滑动窗口算法将失效,此时可能需要用前缀和+二分查找等方法。
五、复杂度分析
-
时间复杂度 O(n):左、右指针各遍历数组一次,每个元素最多被加入窗口和移出窗口一次,总操作次数为 O(n)。
-
空间复杂度 O(1):仅使用常数个变量(left、right、sum、res),不额外占用空间。
六、总结
LeetCode 209 题的核心是掌握滑动窗口算法的应用场景——当需要寻找连续子数组的最优解(最小长度、最大和等),且数组元素具备单调性(如全为正)时,滑动窗口是高效解法。解题关键在于:明确窗口扩张和收缩的条件,动态维护窗口和与最小长度,同时做好边界情况处理。
建议大家结合优化后的代码反复调试,理解双指针的移动逻辑,后续遇到类似的连续子数组问题(如 LeetCode 3 题无重复字符的最长子串),就能快速联想到滑动窗口思路。