[硬核] 双指针进阶:从“对撞”到“滑动窗口”的思维跃迁

0 阅读4分钟

前言

在上一篇关于“三数之和”的探讨中,我们深入剖析了如何利用排序对撞双指针(Left 指针向右,Right 指针向左),将 

O(n3)O(n3)

 的暴力解法优化至 

O(n2)O(n2)

然而,在前端算法面试中,还有另一类涉及数组和字符串处理的高频题目,它们同样依赖双指针,但指针的移动方向却不再是“相对而行”,而是“同向而行”。这种特殊的双指针模式,就是滑动窗口(Sliding Window)

本文将以 LeetCode 209 题“长度最小的子数组”为例,剖析从暴力法到滑动窗口的思维路径,并重点讲解如何进行均摊复杂度分析。

一、 模式对比:对撞 vs 滑动

在进入具体题目前,我们需要建立抽象的几何直觉。

  • 对撞指针(如三数之和)

    • 场景:数组有序,求固定和。
    • 形态:L -> ... <- R。区间两端向中间挤压。
    • 逻辑:根据当前和与目标值的大小,决定是舍弃左边(变大)还是舍弃右边(变小)。
  • 滑动窗口(同向双指针)

    • 场景:寻找满足条件的连续子数组/子串。
    • 形态:[L ... R] ->。像一只毛毛虫,R 先走(扩张),L 随后(收缩)。
    • 逻辑:R 负责寻找可行解,L 负责优化解(寻找最短/最优)。

屏幕录制 2026-02-05 185039.gif

二、 题目解析:长度最小的子数组

题目描述
给定一个含有 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 维护一个窗口。

  1. 扩张:right 主动向右走,累加窗口内的和 sum。
  2. 收缩:当 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;
};

屏幕录制 2026-02-05 190406.gif

三、 核心难点:时间复杂度分析

这是一个面试中的高频陷阱。

很多候选人看到代码中有一个 while (right < len) 循环,内部嵌套了一个 while (sum >= target) 循环,会下意识地认为时间复杂度是 

O(n2)O(n2)

这是错误的。

分析逻辑
时间复杂度看的是基本操作的执行次数
在整个算法运行过程中:

  1. right 指针:从 0 走到 n-1,每个元素进窗口 1 次。总共移动 n 次。
  2. 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++;
    }
}

从“三数之和”的对撞指针,到“最小子数组”的滑动窗口,我们完成了从静态位置判定动态区间维护的进阶。掌握了这两个模型,就攻克了数组算法题的半壁江山。