文章目录
- [LeetCode 209 - 长度最小的子数组算法详解](#LeetCode 209 - 长度最小的子数组算法详解)
-
- 题目描述
- 最优解法:滑动窗口(双指针)
- [进阶解法:前缀和 + 二分查找(O(n log n))](#进阶解法:前缀和 + 二分查找(O(n log n)))
-
- 算法思路
- Java实现
- [算法可视化演示(O(n log n)解法)](#算法可视化演示(O(n log n)解法))
-
- [示例:target = 7, nums = [2,3,1,2,4,3]](#示例:target = 7, nums = [2,3,1,2,4,3])
- 二分查找过程详解(以i=4为例)
- 两种解法对比
- [O(n log n)解法的应用场景](#O(n log n)解法的应用场景)
LeetCode 209 - 长度最小的子数组算法详解
题目描述
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的子数组 [nums[l], nums[l+1], ..., nums[r-1], nums[r]] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
最优解法:滑动窗口(双指针)
算法思路
这是一个典型的滑动窗口问题。我们使用两个指针维护一个可变长度的窗口:
- 右指针(right):扩展窗口,增加元素
- 左指针(left):收缩窗口,移除元素
- 核心思想:当窗口内元素和 >= target时,尝试收缩左边界来寻找更短的满足条件的子数组
Java解决方案
java
public class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int left = 0; // 左指针,窗口左边界
int sum = 0; // 当前窗口内元素和
int minLen = Integer.MAX_VALUE; // 记录最小长度
// 右指针遍历数组
for (int right = 0; right < n; right++) {
sum += nums[right]; // 扩展窗口,加入右边元素
// 当窗口和 >= target时,尝试收缩左边界
while (sum >= target) {
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left]; // 收缩窗口,移除左边元素
left++; // 左指针右移
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
}
关键变量含义
变量名 | 含义 | 作用 |
---|---|---|
left |
滑动窗口的左边界索引 | 控制窗口的起始位置,收缩窗口时右移 |
right |
滑动窗口的右边界索引 | 在for循环中遍历,扩展窗口 |
sum |
当前窗口内所有元素的和 | 判断是否满足 >= target 的条件 |
minLen |
满足条件的最小子数组长度 | 记录并更新找到的最小长度 |
算法可视化演示
示例:target = 7, nums = [2,3,1,2,4,3]
初始状态:
数组: [2, 3, 1, 2, 4, 3]
索引: 0 1 2 3 4 5
left = 0, right = 0, sum = 0, minLen = ∞
第1步:right = 0
数组: [2, 3, 1, 2, 4, 3]
↑
left,right
sum = 0 + 2 = 2
sum < target(7),继续扩展
第2步:right = 1
数组: [2, 3, 1, 2, 4, 3]
↑ ↑
left right
sum = 2 + 3 = 5
sum < target(7),继续扩展
第3步:right = 2
数组: [2, 3, 1, 2, 4, 3]
↑ ↑
left right
sum = 5 + 1 = 6
sum < target(7),继续扩展
第4步:right = 3
数组: [2, 3, 1, 2, 4, 3]
↑ ↑
left right
sum = 6 + 2 = 8
sum >= target(7)!进入while循环收缩
收缩阶段1:
窗口: [2, 3, 1, 2],长度 = 4
minLen = min(∞, 4) = 4
sum = 8 - 2 = 6, left = 1
sum < target,退出while
第5步:right = 4
数组: [2, 3, 1, 2, 4, 3]
↑ ↑
left right
sum = 6 + 4 = 10
sum >= target(7)!进入while循环收缩
收缩阶段2:
窗口: [3, 1, 2, 4],长度 = 4
minLen = min(4, 4) = 4
sum = 10 - 3 = 7, left = 2
sum >= target,继续收缩
收缩阶段3:
窗口: [1, 2, 4],长度 = 3
minLen = min(4, 3) = 3
sum = 7 - 1 = 6, left = 3
sum < target,退出while
第6步:right = 5
数组: [2, 3, 1, 2, 4, 3]
↑ ↑
left right
sum = 6 + 3 = 9
sum >= target(7)!进入while循环收缩
收缩阶段4:
窗口: [2, 4, 3],长度 = 3
minLen = min(3, 3) = 3
sum = 9 - 2 = 7, left = 4
sum >= target,继续收缩
收缩阶段5:
窗口: [4, 3],长度 = 2
minLen = min(3, 2) = 2
sum = 7 - 4 = 3, left = 5
sum < target,退出while
最终结果:minLen = 2
代码分支覆盖分析
上述可视化演示覆盖了代码的所有分支:
- for循环遍历:right从0到5,遍历整个数组
- sum < target分支:步骤1-3展示了sum小于target时继续扩展的情况
- sum >= target分支:步骤4-6展示了满足条件时的处理
- while循环收缩:展示了多次收缩窗口的过程
- minLen更新:展示了长度从4→3→2的更新过程
- 最终返回:返回找到的最小长度2
时间复杂度分析
-
时间复杂度:O(n)
- 虽然有嵌套的while循环,但每个元素最多被访问两次(一次加入,一次移除)
- left和right指针都只会单调递增,总的移动次数不超过2n
-
空间复杂度:O(1)
- 只使用了常数个额外变量
边界情况处理
-
无解情况:如果数组所有元素和都小于target,返回0
javareturn minLen == Integer.MAX_VALUE ? 0 : minLen;
-
单元素解:如果数组中某个元素 >= target,最小长度为1
-
整个数组:如果需要整个数组才能满足条件,返回数组长度
算法优势
- 效率高:一次遍历,O(n)时间复杂度
- 空间优:O(1)空间复杂度
- 逻辑清晰:双指针滑动窗口,易于理解和实现
- 适用性强:可以应用于类似的子数组问题
这个算法体现了滑动窗口技巧的精髓:动态调整窗口大小,在满足条件的前提下寻找最优解。
进阶解法:前缀和 + 二分查找(O(n log n))
算法思路
虽然滑动窗口已经是最优解,但题目要求的O(n log n)解法也有其价值:
- 前缀和:预处理数组,快速计算任意子数组的和
- 二分查找:对于每个起始位置,二分查找最小的结束位置
Java实现
java
public class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
// 构建前缀和数组
int[] prefixSum = new int[n + 1];
for (int i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
int minLen = Integer.MAX_VALUE;
// 枚举每个起始位置
for (int i = 0; i < n; i++) {
// 二分查找最小的结束位置j,使得sum[i...j] >= target
int targetSum = target + prefixSum[i];
int pos = binarySearch(prefixSum, targetSum);
if (pos <= n) {
minLen = Math.min(minLen, pos - i);
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
// 二分查找第一个 >= target的位置
private int binarySearch(int[] arr, int target) {
int left = 0, right = arr.length - 1;
int result = arr.length;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] >= target) {
result = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
return result;
}
}
算法可视化演示(O(n log n)解法)
示例:target = 7, nums = [2,3,1,2,4,3]
第1步:构建前缀和数组
原数组: [2, 3, 1, 2, 4, 3]
索引: 0 1 2 3 4 5
前缀和数组: [0, 2, 5, 6, 8, 12, 15]
索引: 0 1 2 3 4 5 6
第2步:枚举起始位置i=0
需要找到最小的j,使得 prefixSum[j] - prefixSum[0] >= 7
即:prefixSum[j] >= 7 + 0 = 7
二分查找prefixSum数组中第一个 >= 7的位置:
[0, 2, 5, 6, 8, 12, 15]
↑
pos=4
子数组长度 = 4 - 0 = 4
第3步:枚举起始位置i=1
需要找到最小的j,使得 prefixSum[j] - prefixSum[1] >= 7
即:prefixSum[j] >= 7 + 2 = 9
二分查找prefixSum数组中第一个 >= 9的位置:
[0, 2, 5, 6, 8, 12, 15]
↑
pos=5
子数组长度 = 5 - 1 = 4
第4步:枚举起始位置i=2
需要找到最小的j,使得 prefixSum[j] - prefixSum[2] >= 7
即:prefixSum[j] >= 7 + 5 = 12
二分查找prefixSum数组中第一个 >= 12的位置:
[0, 2, 5, 6, 8, 12, 15]
↑
pos=5
子数组长度 = 5 - 2 = 3
第5步:枚举起始位置i=3
需要找到最小的j,使得 prefixSum[j] - prefixSum[3] >= 7
即:prefixSum[j] >= 7 + 6 = 13
二分查找prefixSum数组中第一个 >= 13的位置:
[0, 2, 5, 6, 8, 12, 15]
↑
pos=6
子数组长度 = 6 - 3 = 3
第6步:枚举起始位置i=4
需要找到最小的j,使得 prefixSum[j] - prefixSum[4] >= 7
即:prefixSum[j] >= 7 + 8 = 15
二分查找prefixSum数组中第一个 >= 15的位置:
[0, 2, 5, 6, 8, 12, 15]
↑
pos=6
子数组长度 = 6 - 4 = 2
最终结果:minLen = 2
二分查找过程详解(以i=4为例)
查找prefixSum数组中第一个 >= 15的位置:
数组: [0, 2, 5, 6, 8, 12, 15]
索引: 0 1 2 3 4 5 6
第1次二分:
left=0, right=6, mid=3
arr[3]=6 < 15,left = mid+1 = 4
第2次二分:
left=4, right=6, mid=5
arr[5]=12 < 15,left = mid+1 = 6
第3次二分:
left=6, right=6, mid=6
arr[6]=15 >= 15,result=6,right = mid-1 = 5
left > right,结束,返回result=6
两种解法对比
特性 | 滑动窗口(O(n)) | 前缀和+二分(O(n log n)) |
---|---|---|
时间复杂度 | O(n) | O(n log n) |
空间复杂度 | O(1) | O(n) |
实现难度 | 中等 | 较高 |
适用场景 | 一般推荐 | 多次查询、离线处理 |
O(n log n)解法的应用场景
- 多次查询:如果需要对同一数组进行多次不同target的查询
- 教学价值:展示前缀和与二分查找的结合使用
- 变种问题:某些子数组问题可能更适合这种方法
虽然在这个问题中O(n)的滑动窗口解法更优,但掌握O(n log n)的解法有助于理解算法设计的多样性。