力扣解题-209. 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入:target = 4, nums = [1,4,4]
输出:1
示例 3:
输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0
提示:
1 <= target <= 10⁹
1 <= nums.length <= 10⁵
1 <= nums[i] <= 10⁴
进阶: 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。
Related Topics
数组、二分查找、前缀和、滑动窗口
示例解答
解题思路
核心方法:滑动窗口(双指针)法,通过右指针扩展窗口、左指针收缩窗口,在一次遍历中找到满足条件的最小长度子数组,时间复杂度O(n)、空间复杂度O(1),是本题的最优解法。
核心原理铺垫(滑动窗口的合理性)
由于数组中所有元素都是正整数,因此窗口内的和具有“单调性”:
- 右指针右移 → 窗口扩大 → 窗口和递增;
- 左指针右移 → 窗口缩小 → 窗口和递减。 基于此特性,可通过“扩大窗口找满足条件的解,缩小窗口优化解”的思路,避免暴力枚举所有子数组。
核心逻辑拆解
- 变量定义:
sum:当前滑动窗口内的元素和(初始为0);left:窗口左边界(初始为0);min:记录满足条件的最小窗口长度(初始为Integer.MAX_VALUE,表示未找到);
- 右指针扩展窗口:
- 遍历右指针
right,将nums[right]加入sum,扩大窗口;
- 遍历右指针
- 左指针收缩窗口:
- 当
sum ≥ target时,说明当前窗口满足条件,进入循环收缩左边界:- 计算当前窗口长度
right-left+1,更新min为更小值; - 从
sum中减去nums[left],左指针右移(缩小窗口),尝试找到更短的满足条件的窗口;
- 计算当前窗口长度
- 当
- 结果处理:
- 若
min仍为Integer.MAX_VALUE(未找到满足条件的子数组),返回0; - 否则返回
min。
- 若
具体步骤(以示例1 target=7,nums=[2,3,1,2,4,3]为例)
| right | nums[right] | sum | sum≥target | 窗口范围 | 窗口长度 | min | 左指针收缩操作 |
|---|---|---|---|---|---|---|---|
| 0 | 2 | 2 | 否 | [0,0] | - | MAX | 无 |
| 1 | 3 | 5 | 否 | [0,1] | - | MAX | 无 |
| 2 | 1 | 6 | 否 | [0,2] | - | MAX | 无 |
| 3 | 2 | 8 | 是 | [0,3] | 4 | 4 | sum-2=6,left=1(sum<7,退出收缩) |
| 4 | 4 | 10 | 是 | [1,4] | 4 | 4 | sum-3=7 → 窗口长度3(min=3),sum-1=6,left=3(sum<7,退出) |
| 5 | 3 | 9 | 是 | [3,5] | 3 | 3 | sum-2=7 → 窗口长度2(min=2),sum-4=3,left=5(sum<7,退出) |
最终min=2,返回2,与示例结果一致。 |
性能说明
- 时间复杂度:O(n)(每个元素最多被右指针访问一次、左指针访问一次,总操作数为2n);
- 空间复杂度:O(1)(仅使用3个变量,无额外数组/集合开销);
- 核心优势:
- 利用正整数的单调性,避免暴力法O(n²)的时间复杂度;
- 一次遍历完成所有操作,无冗余计算;
- 空间开销极小,适合处理n=10⁵的大数据量。
public int minSubArrayLen(int target, int[] nums) {
int sum=0;
int left=0;
int min=Integer.MAX_VALUE;
for(int right=0;right<nums.length;right++){
sum=sum+nums[right];
while(sum>=target){
min=Math.min(min,right-left+1);
sum=sum-nums[left];
left++;
}
}
// 如果 min 未被更新,说明无解
return min == Integer.MAX_VALUE ? 0 : min;
}
拓展解法:前缀和+二分查找法(进阶要求)
核心方法:前缀和数组 + 二分查找,先构建前缀和数组,再对每个前缀和,通过二分查找找到满足“前缀和差值≥target”的最小下标,时间复杂度O(n logn),满足进阶要求。
核心原理
- 前缀和数组:
prefix[i]表示前i个元素的和(prefix[0]=0,prefix[1]=nums[0],prefix[2]=nums[0]+nums[1]...),前缀和数组严格递增(因元素为正整数); - 二分查找:对于每个
prefix[right],查找最小的left使得prefix[right] - prefix[left] ≥ target,此时子数组长度为right-left; - 最小长度:遍历所有
right,记录最小的right-left。
代码实现
import java.util.Arrays;
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int[] prefix = new int[n + 1];
// 构建前缀和数组
for (int i = 0; i < n; i++) {
prefix[i + 1] = prefix[i] + nums[i];
}
int min = Integer.MAX_VALUE;
// 遍历每个前缀和,二分查找满足条件的最小left
for (int right = 1; right <= n; right++) {
int targetSum = prefix[right] - target;
// 在前缀和数组的[0, right]范围内找≥targetSum的最小下标
int left = Arrays.binarySearch(prefix, 0, right, targetSum);
// 处理binarySearch的返回值(未找到时返回-(插入点)-1)
if (left < 0) {
left = -left - 1;
}
// 找到有效left,更新最小长度
if (left < right) {
min = Math.min(min, right - left);
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
性能说明
- 时间复杂度:O(n logn)(构建前缀和O(n),遍历+二分查找O(n logn));
- 空间复杂度:O(n)(存储前缀和数组);
- 适用场景:满足进阶要求,适合理解“前缀和+二分”的解题思路,虽性能略低于滑动窗口,但拓展性更强(如处理非正整数数组时,滑动窗口不再适用,二分法仍可尝试)。
对比:暴力法(基础思路,仅作参考)
核心方法:枚举所有子数组,遍历每个起点,累加元素和直到≥target,记录最小长度,逻辑直观但性能极差。
代码实现
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int min = Integer.MAX_VALUE;
// 枚举所有起点
for (int i = 0; i < n; i++) {
int sum = 0;
// 枚举终点
for (int j = i; j < n; j++) {
sum += nums[j];
if (sum >= target) {
min = Math.min(min, j - i + 1);
break; // 元素为正,后续更长,无需继续
}
}
}
return min == Integer.MAX_VALUE ? 0 : min;
}
性能说明
- 时间复杂度:O(n²)(最坏情况每个起点都要遍历到数组末尾),n=10⁵时会超时;
- 空间复杂度:O(1);
- 适用场景:仅适合理解问题本质,实际工程中绝对不推荐使用。
总结
- 滑动窗口法(最优解):O(n)时间+O(1)空间,利用正整数的单调性,一次遍历完成,工程首选;
- 前缀和+二分查找法:O(n logn)时间+O(n)空间,满足进阶要求,拓展性更强;
- 暴力法:O(n²)时间+O(1)空间,仅作思路参考,性能极差;
- 关键技巧:
- 滑动窗口的核心:利用“正整数和的单调性”,收缩左边界优化解;
- 二分查找的核心:前缀和数组的严格递增性,保证二分的有效性;
- 边界处理:初始min设为极大值,最终需判断是否找到有效解(返回0或min)。