代码随想录 第一章 数组 209.长度最小的子数组
代码随想录 第一章 数组 209.长度最小的子数组
题目: 209.长度最小的子数组
-
题目描述:
给定一个含有
n
个正整数的数组和一个正整数target
。找出该数组中满足其总和大于等于
target
的长度最小的 子数组[numsₗ, numsₗ₊₁, ..., numsᵣ₋₁, numsᵣ]
,并返回其长度 。 如果不存在符合条件的子数组,返回0
。
一、思想
滑动窗口的核心思想是就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果。
在本题中实现滑动窗口,主要确定如下三点:
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
窗口就是 满足其和 ≥ target
的长度最小的 连续 子数组。
窗口的起始位置如何移动:如果当前窗口的值大于等于target
了,窗口就要向前移动了(也就是该缩小了)。
窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。
解题的关键在于窗口的起始位置如何移动。
滑动窗口算法详解(示例:nums=[2,3,1,2,4,3]
, target=7
)
右指针 | 当前元素 | 窗口范围 | 累计和 | 是否触发收缩 | 操作记录 | 最小长度 | 左指针位置变化轨迹 |
---|---|---|---|---|---|---|---|
0 | 2 | [0,0] | 2 | 否 | 右移 → | ∞ | 0 → 0 |
1 | 3 | [0,1] | 5 | 否 | 右移 → | ∞ | 0 → 0 |
2 | 1 | [0,2] | 6 | 否 | 右移 → | ∞ | 0 → 0 |
3 | 2 | [0,3] | 8 | 是 | ① 长度=4 → subLength=4 ② sum-=2 → sum=6 ③ 左指针移动 → left=1 | 4 | 0 → 1 |
4 | 4 | [1,4] | 10 | 是 | ① 长度=4 → 不更新 ② sum-=3 → sum=7 ③ 左移 → left=2 ④ 再次检测 → 长度=3 → subLength=3 ⑤ sum-=1 → sum=6 ⑥ 左指针移动→ left=3 | 3 | 1 → 3 |
5 | 3 | [3,5] | 9 | 是 | ① 长度=3 → 不更新 ② sum-=2 → sum=7 ③ 左移 → left=4 ④ 再次检测 → 长度=2 → subLength=2 ⑤ sum-=4 → sum=3 ⑥ 左指针移动→ left=5 | 2 | 3 → 5 |
二、代码
/**
* 类Solution:解决问题
*/
class Solution
{
public:
/**
* 函数minSubArrayLen:寻找最短的子数组,其和至少为target
* @param target 目标和
* @param nums 整数数组
* @return 返回最短子数组的长度,如果不存在满足条件的子数组,返回0
*/
int minSubArrayLen(int target, vector<int> &nums)
{
// 初始化结果为最大整数
int result = INT_MAX;
// 初始化当前和为0
int sum = 0;
// 初始化子数组的起始位置为0
int i = 0;
// 初始化子数组的长度为0
int subLength = 0;
// 遍历数组
for (int j = 0; j < nums.size(); j++)
{
// 更新当前和
sum += nums[j];
// 当当前和大于等于目标和时
while (sum >= target)
{
// 更新子数组的长度
subLength = j - i + 1;
// 更新结果为当前结果和子数组长度的最小值
result = result < subLength ? result : subLength;
// 更新当前和
sum -= nums[i++];
}
}
// 如果结果仍为最大整数,返回0,否则返回结果
return result == INT_MAX ? 0 : result;
}
};
三、解析
1. 算法工作原理分解
1.1 初始化阶段
-
核心变量定义
int result = INT_MAX; // 最小长度记录器(初始设为极大值) int sum = 0; // 窗口和累计器 int i = 0; // 左指针(窗口起始位置)
-
初始状态
- 窗口范围:
[0, 0)
(空窗口) - 示例:
nums=[2,3,1,2,4,3]
初始时sum=0
- 窗口范围:
1.2 右指针主导窗口扩展
-
遍历机制
for(int j=0; j<nums.size(); j++) { // j为右指针 sum += nums[j]; // 动态扩展窗口右边界
-
操作特点
- 右指针
j
单向前进(从0到n-1) - 每次扩展必然添加新元素到窗口
- 示例:当
j=3
时,窗口扩展为[2,3,1,2]
- 右指针
1.3 收缩条件检测
-
触发条件
while(sum >= target) { // 必须用while循环
-
检测逻辑
- 当窗口和
sum
≥target
时进入收缩阶段 - 示例:
j=3
时sum=8 ≥7
触发收缩
- 当窗口和
1.4 窗口收缩与最小长度更新
-
收缩操作流程
-
计算当前窗口长度
subLength = j - i + 1; // 闭区间长度计算
-
更新最小长度
result = min(result, subLength); // 取历史最小值
-
左指针右移
sum -= nums[i++]; // 先减元素值,再移动左指针
-
-
操作示例
-
当窗口为
[2,3,1,2]
时:- 计算长度
4
→ 更新result=4
- 收缩后窗口变为
[3,1,2]
,sum=6
- 计算长度
-
1.5 连续收缩机制
-
多轮收缩场景
while(sum >= target) { // 可能执行多次收缩 // 上述收缩操作 }
-
必要性
-
确保找到以当前右指针为终点的最小窗口
-
示例:当
j=4
时窗口[1,4]
需连续收缩两次- 第一轮收缩:窗口
[2,4]
,sum=7
- 第二轮收缩:窗口
[3,4]
,sum=6
(退出循环)
- 第一轮收缩:窗口
-
1.6 最终结果处理
-
无效结果判断
return (result == INT_MAX) ? 0 : result;
-
逻辑意义
- 若从未找到有效窗口(
result
未更新),返回0 - 示例最终结果:
result=2
(窗口[4,3]
)
- 若从未找到有效窗口(
2. 关键点说明
操作阶段 | 时间复杂度保障机制 | 示例中的操作表现 |
---|---|---|
右指针扩展 | 单层for 循环 → O(n) | 右指针完整遍历数组(6次移动) |
左指针收缩 | 每个元素最多被左指针访问一次 → O(n) | 左指针共移动4次(0→1→2→3→5) |
窗口计算 | 即时更新result → O(1) 操作 | 3次有效更新(4→3→2) |
循环检测 | while 确保完全收缩 → 无遗漏 | 步骤5中触发两次连续收缩 |
四、复杂度分析
- 时间复杂度:O(n)
滑动窗口的 左指针 left
和右指针 right
均单向移动,每个元素最多被访问两次(右指针扩展时访问一次,左指针收缩时访问一次)。对于长度为n
的数组,总操作次数为2n
次,最终时间复杂度为线性级别。 - 空间复杂度:O(1)
仅使用固定数量的额外空间(sum
、left
、result
等整型变量),空间消耗与输入规模n
无关。