大家好,我是你们的算法小伙伴。今天我们来练习一道滑动窗口的经典高频题 ------LeetCode 209. 长度最小的子数组。这道题考察滑动窗口(双指针)的核心思想,同时还附带了一个进阶的二分查找优化方案,是面试中考察数组优化的经典题目。
题目描述
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
进阶: 如果你已经实现 O(n) 时间复杂度的解法,请尝试设计一个 O(n log n) 时间复杂度的解法。
示例 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^91 <= nums.length <= 10^51 <= nums[i] <= 10^4
解题思路
核心特性拆解
数组的核心特性:所有元素为正整数。
- 正整数的特性保证了:窗口向右扩张时,和一定增大;窗口向左收缩时,和一定减小,这是滑动窗口成立的关键前提。
方法一:滑动窗口(双指针,O (n))
思路:用左右指针维护一个窗口,记录窗口内元素的和:
- 右指针向右移动,将元素加入窗口,累加和;
- 当窗口和 ≥ target 时,尝试收缩左指针,尽可能缩小窗口长度,同时更新最小长度;
- 重复上述过程,直到右指针遍历完整个数组。
方法二:前缀和 + 二分查找(O (n log n))
思路:利用前缀和数组,将问题转化为「在有序数组中找满足条件的最小长度」,适合数组元素有正有负、无法用滑动窗口的场景,本题作为进阶方案。
- 计算前缀和数组
prefix,其中prefix[i]表示前i个元素的和; - 遍历前缀和数组,对每个
prefix[i],用二分查找找最小的j > i,使得prefix[j] - prefix[i] ≥ target,计算窗口长度j - i并更新最小值。
代码实现
方法一:滑动窗口
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++;
}
}
// 如果minLen未更新,说明无符合条件的子数组,返回0
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
}
方法二:前缀和 + 二分查找
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int[] prefix = new int[n + 1]; // 前缀和数组,prefix[0] = 0
// 计算前缀和
for (int i = 1; i <= n; i++) {
prefix[i] = prefix[i - 1] + nums[i - 1];
}
int minLen = Integer.MAX_VALUE;
// 遍历前缀和,二分查找
for (int i = 0; i < n; i++) {
int need = target + prefix[i]; // 需要找到 >= need 的前缀和
// 二分查找第一个 >= need 的位置
int left = i + 1, right = n;
while (left <= right) {
int mid = left + (right - left) / 2;
if (prefix[mid] >= need) {
right = mid - 1;
} else {
left = mid + 1;
}
}
// 找到符合条件的位置,更新最小长度
if (left <= n) {
minLen = Math.min(minLen, left - i);
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
}
代码详解
一、滑动窗口法
- 指针初始化 :
left = 0(窗口左边界),sum = 0(窗口和),minLen = Integer.MAX_VALUE(初始设为极大值)。 - 右指针扩张 :遍历数组,将
nums[right]加入窗口,累加和。 - 左指针收缩 :当
sum >= target时,不断收缩左指针,每次收缩都更新最小长度,直到sum < target。 - 结果处理 :若
minLen仍为极大值,说明无符合条件的子数组,返回0,否则返回minLen。
示例 1
模拟 :target = 7, nums = [2,3,1,2,4,3]
| 步骤 | left | right | sum | 窗口 | sum >= 7? | minLen | 操作 |
|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 | 2 | [2] | 否 | MAX | right++ |
| 2 | 0 | 1 | 5 | [2,3] | 否 | MAX | right++ |
| 3 | 0 | 2 | 6 | [2,3,1] | 否 | MAX | right++ |
| 4 | 0 | 3 | 8 | [2,3,1,2] | 是 | 4 | sum-=2, left=1 |
| 5 | 1 | 3 | 6 | [3,1,2] | 否 | 4 | right++ |
| 6 | 1 | 4 | 10 | [3,1,2,4] | 是 | 4 → 4 | sum-=3, left=2 |
| 7 | 2 | 4 | 7 | [1,2,4] | 是 | 3 | sum-=1, left=3 |
| 8 | 3 | 4 | 6 | [2,4] | 否 | 3 | right++ |
| 9 | 3 | 5 | 9 | [2,4,3] | 是 | 3 → 3 | sum-=2, left=4 |
| 10 | 4 | 5 | 7 | [4,3] | 是 | 3 → 2 | sum-=4, left=5 |
| 11 | 5 | 5 | 3 | [3] | 否 | 2 | 循环结束 |
最终返回 2,与示例结果一致。
二、前缀和 + 二分查找法
- 前缀和计算 :
prefix[0] = 0,prefix[i] = prefix[i-1] + nums[i-1],保证前缀和数组严格递增(数组元素为正)。 - 二分查找 :对每个
prefix[i],计算需要的目标和need = target + prefix[i],在i+1到n区间内二分查找第一个>= need的prefix[j],窗口长度为j - i。 - 优势 :不依赖数组元素全为正,适用于更通用的场景,时间复杂度
O(n log n)。
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 滑动窗口法 | O(n) |
O(1) |
数组元素全为正,最优解 |
| 前缀和 + 二分查找 | O(n log n) |
O(n) |
数组元素有正有负,通用解法 |
总结
- 核心考点 :本题的核心是 ** 滑动窗口(双指针)** 的应用,利用数组元素全为正的特性,将暴力枚举的
O(n²)复杂度优化到O(n),是面试中考察数组优化的经典题目。 - 关键逻辑:滑动窗口的核心是「右指针扩张,左指针收缩」,只有当窗口和满足条件时才收缩左指针,保证每个元素最多被访问两次,时间复杂度线性。
- 进阶拓展:前缀和 + 二分查找是通用解法,不依赖元素全为正,适合处理更复杂的子数组和问题,是面试中拓展思路的好方向。
- 易错点 :
- 初始
minLen设为Integer.MAX_VALUE,避免遗漏边界情况; - 窗口长度计算为
right - left + 1,不要漏加1; - 最终返回时需判断
minLen是否更新,未更新则返回0。
- 初始
今天的每日算法练习就到这里,我们明天再见!👋