前言
各位掘友们好!今天我们来聊聊一个经典的算法问题——LeetCode 209题「长度最小的子数组」。这道题就像是算法界的"减肥神器",教你如何用最短的子数组达到目标和,简直是数组界的"断舍离"哲学!
题目回顾
根据 leetcode209 的描述,题目要求我们:
给定一个含有 n 个正整数的数组和一个正整数 target,找出该数组中满足其总和大于等于 target 的长度最小的子数组,并返回其长度。
简单来说,就是要找到一个"最短路径"来达成目标,有点像找最优解的感觉。
示例分析:
- 输入:
target = 7, nums = [2,3,1,2,4,3] - 输出:
2(子数组[4,3]的和为7,长度为2)
解法一:暴力破解(新手村装备)
最直观的想法就是暴力枚举所有可能的子数组,就像是"地毯式搜索":
// 暴力解法(时间复杂度 O(n²))
var minSubArrayLenBrute = function(target, nums) {
let minLength = Infinity;
for (let i = 0; i < nums.length; i++) {
let sum = 0;
for (let j = i; j < nums.length; j++) {
sum += nums[j];
if (sum >= target) {
minLength = Math.min(minLength, j - i + 1);
break; // 找到就可以跳出了
}
}
}
return minLength === Infinity ? 0 : minLength;
};
这种方法虽然能解决问题,但效率堪比"用筷子吃汤"——能行,但不优雅。
解法二:滑动窗口(高手进阶装备)
接下来我们看看滑动窗口解法,这才是真正的"神器":
var minSubArrayLen = function(target, nums) {
let left = 0; // 左指针,初始指向数组起始位置
let sum = 0; // 当前窗口的和
let minLength = Infinity; // 记录最小长度,初始化为无穷大
for (let right = 0; right < nums.length; right++) {
sum += nums[right]; // 扩展右边界,累加当前元素到窗口和
// 当窗口和大于等于目标值时,尝试收缩左边界以找到更短的子数组
while (sum >= target) {
// 更新最小长度
minLength = Math.min(minLength, right - left + 1);
// 收缩左边界,减去左指针指向的元素值,并移动左指针
sum -= nums[left];
left++;
}
}
// 如果minLength仍为无穷大,说明没有符合条件的子数组,返回0;否则返回minLength
return minLength === Infinity ? 0 : minLength;
};
滑动窗口的精髓
滑动窗口就像是一个"智能橡皮筋":
- 右指针扩展:不断向右移动,增加窗口大小,就像拉伸橡皮筋
- 左指针收缩:当条件满足时,尝试缩小窗口,寻找最优解
- 动态调整:根据当前状态决定是扩展还是收缩
这种方法的时间复杂度是 O(n),因为每个元素最多被访问两次(一次被右指针访问,一次被左指针访问)。
问题代码分析
让我们看看下面这段代码,这里有一些需要改进的地方:
var minSubArrayLen = function(target, nums) {
let sum = 0, arr = [];
for (let i = 0; i < nums.length; i++) {
arr.push(nums[i]);
sum += nums[i];
while(sum >= target) {
if (sum - arr[0] >= target) {
arr.shift(); // 这里有问题!
}
}
}
if (sum >= target) return arr.length;
else return 0;
};
问题分析:
- 逻辑错误:
while循环中缺少对sum的更新 - 效率问题:使用
arr.shift()的时间复杂度是 O(n) - 结果错误:最后只检查了最终状态,没有记录过程中的最小值
修正版本:
var minSubArrayLenFixed = function(target, nums) {
let sum = 0, arr = [];
let minLength = Infinity;
for (let i = 0; i < nums.length; i++) {
arr.push(nums[i]);
sum += nums[i];
while(sum >= target) {
minLength = Math.min(minLength, arr.length);
sum -= arr.shift(); // 修正:更新sum
}
}
return minLength === Infinity ? 0 : minLength;
};
算法复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 优缺点 |
|---|---|---|---|
| 暴力解法 | O(n²) | O(1) | 简单直观,但效率低 |
| 滑动窗口 | O(n) | O(1) | 高效优雅,推荐使用 |
| 数组模拟 | O(n²) | O(n) | 直观但效率和空间都不佳 |
滑动窗口的应用场景
滑动窗口不仅仅适用于这道题,它是解决"连续子数组/子字符串"问题的万能钥匙:
- 最长无重复字符子串
- 字符串匹配问题
- 固定窗口大小的最值问题
- 可变窗口大小的优化问题
实战技巧
- 双指针维护:左右指针分工明确,右指针负责扩展,左指针负责收缩
- 状态维护:及时更新窗口内的状态(如和、计数等)
- 边界处理:注意空数组、单元素等边界情况
- 优化目标:明确是求最大值、最小值还是计数
总结
滑动窗口算法就像是程序员的"瑞士军刀",看似简单,但威力无穷。它教会我们一个道理:有时候最优解不是通过复杂的算法得到的,而是通过巧妙的思维方式。
记住滑动窗口的核心思想:"能进则进,该退则退",就像人生一样,知进退,懂取舍,方能游刃有余。
希望这篇文章能帮助大家更好地理解滑动窗口算法。如果你觉得有用,别忘了点赞收藏哦!毕竟,好的算法就像好的朋友一样,值得珍藏。