LeetCode 热题 100:滑动窗口

102 阅读4分钟

滑动窗口

滑动窗口(Sliding Window)是一种常用的双指针技巧,主要用于解决数组或字符串的子数组/子串问题,尤其是涉及连续、固定或可变长度、满足某种条件的最值问题(如最长、最短、包含特定元素等)。

基本思想

使用两个指针(通常叫 leftright)维护一个“窗口”:

  • 窗口:由两个指针 left 和 right 定义的连续子数组 [left, right]
  • 滑动right 不断向右扩展(扩大窗口),当窗口内不满足条件时,left 向右收缩(缩小窗口)。
  • 目标:在滑动过程中,记录满足条件的最优解(如最长/最短长度、最大和等)。

时间复杂度通常是 O(n) ,因为每个元素最多被访问两次(进窗口一次,出窗口一次)

通用模板

let left = 0; 
for (let right = 0; right < n; right++) { 
// 1. 将 right 元素加入窗口(更新状态) 
while (窗口不满足条件) { 
    // 2. 移除 left 元素,收缩窗口 
    left++; 
} 
// 3. 此时窗口满足条件,更新答案 
}

1. 无重复字符的最长子串

题目描述

给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。

解题思路

使用可变滑动窗口 + 哈希集合(Set)

  • right 指针不断扩张窗口,将新字符加入 Set
  • 当遇到重复字符(Set.has(c) 为真)时,说明窗口不合法;
  • 此时移动 left 指针,逐个删除左侧字符,直到窗口中不再包含该重复字符;
  • 每次窗口合法时,更新最大长度 ans = max(ans, right - left + 1)

💡 关键洞察:窗口 [left, right] 始终维护一个“无重复字符”的子串。通过收缩左边界,确保每次加入新字符后窗口依然合法。

代码实现

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    let ans = 0;
    let left = 0;
    const window = new Set();

    for (let right = 0; right < s.length; right++) {
        const c = s[right];

        // 若窗口中已有 c,收缩左边界直到无重复
        while (window.has(c)) {
            window.delete(s[left]);
            left++;
        }

        window.add(c);
        ans = Math.max(ans, right - left + 1);
    }

    return ans;
};

复杂度分析

  • 时间复杂度:O(n),每个字符最多被 leftright 各访问一次;
  • 空间复杂度:O(∣Σ∣),其中 ∣Σ∣ 为字符集大小(如 ASCII 为 128)。

2. 找到字符串中所有字母异位词

题目描述

给定两个字符串 s(主串)和 p(模式串),找出 s 中所有 p字母异位词的起始索引。
字母异位词:由相同字符以任意顺序组成的字符串(如 "abc""bca")。

解题思路

使用固定长度滑动窗口 + 哈希表频次匹配

  1. 先统计 p 中每个字符的出现次数,存入 map
  2. 初始化窗口为 s 的前 pLen 个字符,对应减少 map 中的计数;
  3. 若此时 map 中所有值均为 0,说明窗口与 p 是异位词,记录索引 0
  4. 窗口向右滑动:每次移出左侧字符(计数 +1),移入右侧字符(计数 -1);
  5. 每次滑动后检查 map 是否全零,若是则记录当前起始位置。

代码实现

/**
 * @param {string} s
 * @param {string} p
 * @return {number[]}
 */
var findAnagrams = function (s, p) {
    let sLen = s.length
    let pLen = p.length
    let ans = []
    if (pLen > sLen) return ans

    const map = new Map()
    
    // 用 Map 记录 p 中每个字符的频次(目标频次)
    for (let c of p) {
        map.set(c, (map.get(c) || 0) + 1)
    }
    
    // 初始化滑动窗口
    for (let i = 0; i < pLen; i++) {
        if (map.has(s[i])) map.set(s[i], map.get(s[i]) - 1)
    }
    if (isAllZero(map)) ans.push(0)
    
    // 开始滑动窗口
    for(let right = pLen; right<sLen;right++){
        const left = right-pLen
        // 右边界扩展
        if(map.has(s[right]))map.set(s[right],map.get(s[right])-1)
        // 左边界收缩
        if(map.has(s[left]))map.set(s[left],map.get(s[left])+1)
        // 窗口满足条件,更新答案
        if(isAllZero(map)) ans.push(left+1)
    }

    return ans
};
function isAllZero(map) {
    for (let [key, val] of map) {
        if (val !== 0) return false
    }
    return true
}

复杂度分析

  • 时间复杂度:O(n),其中 n 为 s 的长度。初始化窗口 O(m),滑动过程 O(n - m),isAllZero 最坏 O(k)(k 为字符种类),但 k 通常很小(如 26);
  • 空间复杂度:O(k),哈希表存储字符频次。

总结对比

题目窗口类型核心技巧
无重复字符的最长子串可变窗口动态去重,遇重复收缩左边界
找到所有字母异位词固定窗口频次匹配,滑动时更新计数

右扩左缩,维护状态,条件判断,更新答案

  • 右指针:负责探索新元素,扩大窗口
  • 左指针:在满足/不满足条件时收缩窗口
  • 状态维护:用哈希表、计数器、sum 等记录窗口信息
  • 结果更新:在合适时机(通常在收缩过程中)更新最优解

掌握这两种模式,你就能轻松应对 LeetCode 中绝大多数子串类问题。滑动窗口 + 哈希表,是处理字符串连续区间问题的黄金组合 💫。


希望这篇解析对你有帮助!如果你喜欢这类深入浅出的算法讲解,欢迎关注我的 LeetCode 系列文章 👋