前言
在上一篇关于“三数之和”的探讨中,我们深入剖析了如何利用排序和对撞双指针(Left 指针向右,Right 指针向左),将
O(n3)O(n3)
的暴力解法优化至
O(n2)O(n2)
。
然而,在前端算法面试中,还有另一类涉及数组和字符串处理的高频题目,它们同样依赖双指针,但指针的移动方向却不再是“相对而行”,而是“同向而行”。这种特殊的双指针模式,就是滑动窗口(Sliding Window) 。
本文将以 LeetCode 209 题“长度最小的子数组”为例,剖析从暴力法到滑动窗口的思维路径,并重点讲解如何进行均摊复杂度分析。
一、 模式对比:对撞 vs 滑动
在进入具体题目前,我们需要建立抽象的几何直觉。
-
对撞指针(如三数之和) :
- 场景:数组有序,求固定和。
- 形态:L -> ... <- R。区间两端向中间挤压。
- 逻辑:根据当前和与目标值的大小,决定是舍弃左边(变大)还是舍弃右边(变小)。
-
滑动窗口(同向双指针) :
- 场景:寻找满足条件的连续子数组/子串。
- 形态:[L ... R] ->。像一只毛毛虫,R 先走(扩张),L 随后(收缩)。
- 逻辑:R 负责寻找可行解,L 负责优化解(寻找最短/最优)。
二、 题目解析:长度最小的子数组
题目描述:
给定一个含有 n 个正整数的数组 nums 和一个正整数 target 。找出该数组中满足其和 ≥ target 的长度最小的 连续子数组,并返回其长度。如果不存在,返回 0。
1. 暴力解法
最直观的思路是:以每一个元素 nums[i] 为起点,向后寻找,直到和大于等于 target,记录长度,然后对比取最小值。
JavaScript
// 伪代码
for (let i = 0; i < n; i++) {
let sum = 0;
for (let j = i; j < n; j++) {
sum += nums[j];
if (sum >= target) {
minLen = Math.min(minLen, j - i + 1);
break; // 找到以 i 开头的最短解,break
}
}
}
这种解法存在大量的重复计算。当我们从 i 移动到 i+1 时,之前的区间和完全重新计算了,浪费了已知信息。
2. 滑动窗口解法
我们使用两个指针 left 和 right 维护一个窗口。
- 扩张:right 主动向右走,累加窗口内的和 sum。
- 收缩:当 sum >= target 时,说明找到了一个可行解。此时,right 停下,left 开始向右走,试探能否在保持 sum >= target 的前提下缩小窗口长度(优化解)。
JavaScript
/**
* @param {number} target
* @param {number[]} nums
* @return {number}
*/
var minSubArrayLen = function(target, nums) {
let left = 0;
let right = 0;
let sum = 0;
let minLen = Infinity; // 初始化为无穷大
const len = nums.length;
while (right < len) {
// 1. 进窗口:累加当前元素
sum += nums[right];
// 2. 满足条件:尝试收缩窗口,寻找更优解
while (sum >= target) {
// 更新最小长度
minLen = Math.min(minLen, right - left + 1);
// 出窗口:减去左边的元素
sum -= nums[left];
// 左指针右移
left++;
}
// 继续扩张
right++;
}
return minLen === Infinity ? 0 : minLen;
};
三、 核心难点:时间复杂度分析
这是一个面试中的高频陷阱。
很多候选人看到代码中有一个 while (right < len) 循环,内部嵌套了一个 while (sum >= target) 循环,会下意识地认为时间复杂度是
O(n2)O(n2)
。
这是错误的。
分析逻辑:
时间复杂度看的是基本操作的执行次数。
在整个算法运行过程中:
- right 指针:从 0 走到 n-1,每个元素进窗口 1 次。总共移动 n 次。
- left 指针:从 0 走到 n-1(最多),每个元素出窗口 1 次。总共移动 n 次。
结论:
尽管是双层循环,但两个指针都是单调递增的,不会回退。数组中的每个元素最多被操作两次(进一次,出一次)。
总操作次数为 2n2n,常数忽略,因此时间复杂度为 O(n)。
这种分析方法被称为均摊复杂度分析。
四、 总结与通用模板
滑动窗口算法本质上是维护了一组满足特定规则的数据集合。解决此类问题的通用模板如下:
JavaScript
/* 滑动窗口通用模板 */
function slidingWindow(nums) {
let left = 0, right = 0;
while (right < nums.length) {
// c 是将移入窗口的字符
let c = nums[right];
// 1. 右移窗口,进行数据更新
...
// 2. 判断左侧窗口是否要收缩 (根据题目条件:如 sum >= target 或 窗口长度超限)
while (window needs shrink) {
// d 是将移出窗口的字符
let d = nums[left];
// 3. 左移窗口,进行数据更新
...
left++;
}
right++;
}
}
从“三数之和”的对撞指针,到“最小子数组”的滑动窗口,我们完成了从静态位置判定到动态区间维护的进阶。掌握了这两个模型,就攻克了数组算法题的半壁江山。