CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
LeetCode 算法模式/课时/滑动窗口
RawBlame
课时目录12 节

LeetCode 算法模式

  1. 01LeetCode 怎么刷
  2. 02数组与哈希
  3. 03双指针
  4. 04栈
  5. 05二分查找
  6. 06滑动窗口
  7. 07链表
  8. 08二叉树
  9. 09堆
  10. 10回溯
  11. 11图与 Trie
  12. 12DP 与进阶

滑动窗口

无重复子串、最小窗口。

35 分钟

滑动窗口:连续区间的一次扫描

这一课你将能做到

  1. 识别「最长 / 最短 / 连续子数组或子串满足条件」的窗口信号
  2. 写出可变长度窗口的标准骨架:右指针扩张、左指针收缩、窗口内更新答案
  3. 用计数数组 + 缺口计数器解决无重复最长子串和最小覆盖子串

为什么要学

上一课的双指针是两个指针独立运动,滑动窗口是它的一个高频特化:两个指针之间的区间始终代表「当前正在考察的连续子串」,右指针不断把新区间纳入,左指针在条件被破坏 / 需要找最小时把旧区间吐出。

为什么要专门学?因为「连续子数组 / 子串」问题用暴力做通常是 O(n²)(枚举每个起点和终点),而滑动窗口只扫一遍,O(n)。限流时间窗、日志统计最近 5 分钟请求、Kafka 消费速率监控,工程里的「窗口」概念也完全同源。这是中等题里模板化程度最高、投入产出比最好的一课。

先建立图像

字符串:a   b   c   a   b   c   b   b
窗口:      [-------]            ← 左右指针夹住的连续区间
           left   right
right++  → 纳入新字符,尝试扩张
left++   → 条件不满足(或找最短)时,吐出左侧字符

固定骨架(可变长度窗口):

右指针逐格扩张
  更新窗口内的统计
  while(窗口不合法 / 还能继续收缩):
      需要答案时先在这记录(最短类)
      左指针收缩并更新统计
  需要答案时在这记录(最长类)

口诀:求最长,合法时拼命扩张;求最短,合法时拼命收缩。

跟着做

步骤 1:LeetCode 209「长度最小的子数组」(中等,热身)

给定正整数数组和 target,找出和 ≥ target 的最短连续子数组长度,没有则返回 0。

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left = 0;
        int sum = 0;
        int best = Integer.MAX_VALUE;
        for (int right = 0; right < nums.length; right++) {
            sum += nums[right];
            while (sum >= target) {                 // 窗口合法,还能更短
                best = Math.min(best, right - left + 1);
                sum -= nums[left++];               // 吐掉左端,尝试缩短
            }
        }
        return best == Integer.MAX_VALUE ? 0 : best;
    }
}

每个元素至多进出窗口一次,时间 O(n),空间 O(1)。这题展示「最短类」的标准动作:合法 → 记答案 → 收缩。

步骤 2:LeetCode 3「无重复字符的最长子串」(中等)

给定字符串 s,求不含重复字符的最长子串长度,如 "abcabcbb" 答案是 3(abc)。

思路:窗口 [left, right] 始终保持无重复。用计数数组记录窗口内各字符个数;右指针纳入的字符若让计数变成 2,就不断移动左指针,直到这个字符的计数回到 1。

class Solution {
    public int lengthOfLongestSubstring(String s) {
        int[] count = new int[128]; // ASCII
        int left = 0;
        int best = 0;
        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            count[c]++;
            while (count[c] > 1) {           // 新来的字符造成重复
                count[s.charAt(left)]--;
                left++;
            }
            best = Math.max(best, right - left + 1); // 窗口此刻合法
        }
        return best;
    }
}

逐点讲:注意记录答案的位置在收缩完成之后——此时窗口一定无重复。时间 O(n)(left 总共只走 n 步),空间 O(1)(128 固定大小)。

步骤 3:LeetCode 76「最小覆盖子串」(困难,窗口技巧集大成)

给定字符串 s 和 t,在 s 中找包含 t 全部字符(含个数)的最短子串,如 s="ADOBECODEBANC"、t="ABC",答案 "BANC"。

直接模拟「窗口是否覆盖 t」每次要扫一遍计数表,代价高。引入一个 need 变量:当前窗口还缺几种字符(按「该字符个数是否达标」计,一个达标种类算一票)。右指针纳入字符时若让某字符恰好达标,need--;need 为 0 表示全覆盖,开始收缩左端找最短。

class Solution {
    public String minWindow(String s, String t) {
        int[] needCount = new int[128];
        for (char c : t.toCharArray()) needCount[c]++;

        int need = 0; // t 中出现了多少种不同字符
        for (int v : needCount) if (v > 0) need++;

        int[] have = new int[128];
        int left = 0;
        int bestStart = 0;
        int bestLen = Integer.MAX_VALUE;

        for (int right = 0; right < s.length(); right++) {
            char c = s.charAt(right);
            have[c]++;
            if (needCount[c] > 0 && have[c] == needCount[c]) {
                need--; // 这一种字符刚好凑够
            }
            while (need == 0) { // 已全覆盖:收缩左端求最短
                if (right - left + 1 < bestLen) {
                    bestLen = right - left + 1;
                    bestStart = left;
                }
                char d = s.charAt(left);
                if (needCount[d] > 0 && have[d] == needCount[d]) {
                    need++; // 再删就缺这一种了
                }
                have[d]--;
                left++;
            }
        }
        return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLen);
    }
}

逐点讲:判定「恰好达标」必须用 == 而不是 >=——多给的字符不应该重复减 need。时间 O(n),空间 O(1)。这道是窗口类的天花板题,能独立默写,字符串窗口题就基本通关。

讲清楚要点

  1. 窗口题先想「合法条件」怎么表达:和超阈值用累加变量;字符覆盖用计数表 + 缺口种类数;无重复用计数 >1。
  2. 答案记录位置由最长 / 最短决定:最长在窗口合法(收缩完)后更新;最短在 while 合法内部、每次收缩前更新。
  3. 右指针负责扩张,左指针负责恢复合法:两个指针总共各走 n 步,这是 O(n) 的根本原因。
  4. 计数表 128 与 HashMap 的选择:ASCII 字符用数组最快;字符范围不确定(Unicode)再用 HashMap,逻辑一致。
  5. 「恰好」是关键比较:凑够、超标是两件事,最小覆盖子串的 need 变化只发生在恰好达标的瞬间。

你可能会懵的地方

  • 为什么双重 while 还是 O(n)?不会变 O(n²) 吗? 看左指针总共的移动量:它在整个过程中只增不减,最多走 n 格。把所有轮次的 while 体加起来也不超过 n 次,所以整体仍是 O(n)。

  • 无重复子串为什么不能用 Set 就行? 能。遇到重复时要 while 左移并 remove,Set 也能写。计数数组更直接,因为它天然支持「重复了几个」的判断,处理含多次出现的变体(如最多含两个不同字符)也更容易扩展。

  • 最小覆盖子串里 t 有重复字符(如 "AABC")怎么办? needCount 记录的是个数,达标条件是 have[c] == needCount[c],重复字符天然按个数比较,不需要额外处理。

  • 收缩时为什么先判 need++ 再 have--? 要判断「删除前」是否恰好达标:若 have 正好等于 need,删完必然短缺,need++。先减再比就失去判断依据了。

  • 窗口题和双指针题到底怎么区分? 不必严格区分,窗口就是「区间有明确语义」的双指针。学习上只要记住:题目强调连续子串/子数组 + 一个关于区间的条件,优先套窗口骨架。

错法对照

错法问题正解
每次判断覆盖都遍历计数表内层藏着 O(字符集),整体退化维护 need 缺口数,O(1) 判定
达标判断写成 have[c] >= needCount[c]同一种字符被重复计入 need只在 == 时变化
求最长在 while 内更新答案取到的是收缩过程中的短窗口收缩完成后再更新
收缩时先改 have 再判短缺条件依据被破坏先判定,再更新统计
用 HashMap 却忘记删 0 值键干扰达标判断计数用数组或允许保留 0 值

卡点急救

  1. 窗口题无从下手:先写死右指针 for 循环和空的左指针,再问自己「窗口什么时候不合法」,把恢复合法的动作写进 while,骨架就立住了。
  2. 最小覆盖子串 need 变来变去出错:只盯两条规则——have 从 need-1 涨到 need 时 need--;从 need 降到 need-1 时 need++。其他时刻 need 不动。
  3. 答案总是差一:窗口长度统一写 right - left + 1;substring 终点是 start + len(右开)。先在纸上确认下标含义。

面试常问

  • 无重复最长子串如果要求的是「至多包含 K 个不同字符」呢(LeetCode 340)? 把 while 条件换成「窗口内不同字符种类 > K」,用一个变量维护种类数(计数从 0 变 1 时 +1),其余骨架不变。这是面试官最爱出的延展,考你是否理解条件是可插拔的。

  • 滑动窗口为什么要求条件可以「增量维护」? O(n) 的前提是加入/删除一个元素时,窗口统计能在 O(1) 更新。若条件是「窗口内元素的中位数」这类无法 O(1) 维护的量,就要配合堆等结构,单纯窗口就不够了。

  • 数组里有负数时,209 题还能用窗口吗? 不能。负数破坏「扩张窗口和一定增大、收缩一定减小」的单调性,左指针何时收缩无法判定,需要改用前缀和。能用窗口的子数组和题,默认元素为正。

自测三问

  1. 求最长和求最短,更新答案分别在什么位置?为什么?
  2. 最小覆盖子串的 need 变量记录的是什么?为什么能把判定降到 O(1)?
  3. 举一个不能用滑动窗口的子数组问题,并说明原因。

小结一句话

滑动窗口用「右扩左缩」一次扫描搞定连续区间:合法条件能增量维护是前提,最长靠扩张、最短靠收缩,覆盖问题用缺口数代替全表检查。

延伸

  • 巩固练习:LeetCode 340 至多包含 K 个不同字符的最长子串、LeetCode 567 字符串的排列、LeetCode 239 滑动窗口最大值
  • 在本站在线判题平台提交时,构造 t 比 s 长、完全无解的用例检验返回值
  • 下一课预告:链表——指针操作的基本功,反转、判环、合并三个必考题
加载本节练习…
← 上一课二分查找下一课 →链表