知识库算法通用解题范式通用解题范式双指针与滑动窗口双指针与滑动窗口
02 · 通用解题范式通用解题范式
Roadmap 02核心Markdown30 min

双指针与滑动窗口双指针与滑动窗口

用单调移动维护区间不变量,处理子数组与字符串问题。用单调移动维护区间不变量,处理子数组与字符串问题。

#双指针双指针#窗口窗口更新于 2026-08-08

专题导读

双指针通过两个单调移动的位置压缩搜索空间;滑动窗口则在双指针基础上维护一个连续区间及其统计状态。核心不是背模板,而是找到窗口条件的单调性,并明确扩张、收缩时必须保持的不变量。

学习目标

  • 根据输入结构识别相向、同向和快慢指针模式
  • 区分固定窗口与可变窗口,并正确选择收缩时机
  • 用计数器或哈希表 O(1) 更新窗口状态
  • 通过指针总移动次数证明整体复杂度为 O(n)

双指针模式地图

双指针成立的前提通常是有序性、区间连续性,或指针移动后可以永久排除一部分候选。

模式 指针方向 典型信号 代表问题
相向双指针 left → ← right 有序数组、两端决策 两数之和、盛水容器、回文判断
同向双指针 slow → fast → 原地整理、去重、删除 移除元素、合并数组
快慢指针 不同速度同向 链表环、中点、周期 环形链表、寻找中点
滑动窗口 left → right → 连续子数组/子串、条件单调 最短覆盖、最长无重复子串

识别关键词: 题目要求连续区间,且右边界扩张会让某个指标单调增加或变差,通常可以考虑滑动窗口;如果包含负数导致区间和不再单调,应转向前缀和等方法。

先写窗口不变量

可变窗口最重要的是明确:进入循环某个位置时,[left, right] 到底满足什么条件。

求最长合法窗口

right 每次扩张;当窗口非法时用 while 收缩直到重新合法;此时用 right-left+1 更新最大值。

求最短合法窗口

right 扩张直到窗口合法;用 while 尽可能收缩,并在每次收缩前更新最小值。

统计合法窗口数量

若合法性具有单调性,以 right 结尾的合法区间数量通常可由当前 left 直接计算。

状态增量维护

右端进入和左端离开时只更新受影响元素,避免每次重新扫描整个窗口。

while 不会让复杂度变成 O(n²),因为 left 在整个算法中只从 0 移动到 n,每个字符最多进入和离开窗口各一次。

JAVA
import java.util.HashMap;
import java.util.Map;

public final class LongestUniqueSubstring {
    private LongestUniqueSubstring() {
    }

    public static int lengthOfLongestSubstring(String text) {
        Map<Character, Integer> count = new HashMap<>();
        int left = 0;
        int answer = 0;

        for (int right = 0; right < text.length(); right++) {
            char current = text.charAt(right);
            count.merge(current, 1, Integer::sum);

            // 不变量:退出 while 后,窗口内没有重复字符
            while (count.get(current) > 1) {
                char removed = text.charAt(left++);
                count.computeIfPresent(removed, (key, value) -> value - 1);
            }
            answer = Math.max(answer, right - left + 1);
        }
        return answer;
    }
}

// left、right 都最多移动 n 次:时间 O(n),空间 O(字符集)

固定窗口与状态更新

固定长度为 k 时,每轮加入 nums[right],并在窗口超过 k 后移除 nums[right-k],无需维护独立 left 也可以完成。

窗口达到 k 之前不要更新答案;移除元素的下标是 right-k,而不是 right-k+1。

JAVA
public final class FixedWindow {
    private FixedWindow() {
    }

    public static long maxWindowSum(int[] nums, int k) {
        if (k <= 0 || k > nums.length) {
            throw new IllegalArgumentException("k out of range");
        }

        long windowSum = 0L;
        long answer = Long.MIN_VALUE;

        for (int right = 0; right < nums.length; right++) {
            windowSum += nums[right];
            if (right >= k) {
                windowSum -= nums[right - k];
            }
            if (right >= k - 1) {
                answer = Math.max(answer, windowSum);
            }
        }
        return answer;
    }
}

边界、单调性与替代方案

滑动窗口不是所有连续区间题的通解。只有移动边界后,合法性或目标值具有可预测变化时,才能安全排除旧状态。

问题特征 推荐方法 原因
正数数组,最短和 ≥ target 滑动窗口 扩张只增大和,收缩只减小和
包含负数,区间和 = k 前缀和 + 哈希 加入负数后窗口和不再单调
有序数组两数之和 相向双指针 和过大/过小时可排除一侧
无序数组两数之和 哈希表 没有有序性,指针移动无法排除候选
需要窗口最大值 单调队列 普通计数无法 O(1) 删除当前最大值

常见误区

用 if 代替 while 收缩

右边界一次扩张可能导致窗口需要连续收缩多次。除非能证明每轮只需移动一次,否则应使用 while。

更新答案的时机错误

最长合法窗口应在恢复合法后更新;最短合法窗口要在 while 内、移动 left 之前更新。

窗口长度 off-by-one

闭区间 [left,right] 长度是 right-left+1;半开区间 [left,right) 长度才是 right-left。全篇保持统一。

忽略条件单调性

数组含负数时,窗口和随边界移动不再单调,传统滑动窗口可能漏解。

面试追问

Q1:为什么滑动窗口是 O(n) 而不是 O(n²)?

虽然代码中 for 内有 while,但 left 与 right 都只单调向右,每个元素最多进入、离开窗口各一次,总指针移动次数不超过 2n。

Q2:什么时候不能使用滑动窗口?

当移动边界后窗口条件没有单调变化,无法证明被移除状态不可能成为答案时不能使用。例如含负数数组的区间和问题通常改用前缀和与哈希。

Q3:最长窗口和最短窗口的模板有什么区别?

最长问题在窗口非法时收缩,恢复合法后更新最大值;最短问题先扩张到合法,然后在合法期间不断更新答案并收缩,寻找更短区间。