滑动窗口算法题的技巧总结

0 阅读5分钟

一、解题思维总结

1. 何时使用滑动窗口?

场景解法
寻找长度固定的连续子数组的最大/最小值固定窗口滑动
寻找不含重复字符的最长子串可变窗口滑动(双指针)
寻找可以通过k次替换得到的最长重复字符子串可变窗口滑动(双指针+哈希表)
判断一个字符串是否包含另一个字符串的排列固定窗口滑动+哈希表
寻找包含所有目标字符的最小子串可变窗口滑动(双指针+哈希表)
寻找滑动窗口中的最大值双端队列优化滑动窗口

2. 复杂度分析

  • 固定窗口滑动:O(n) 时间复杂度,O(1) 空间复杂度(除存储结果外)
  • 可变窗口滑动:O(n) 时间复杂度,O(k) 空间复杂度(k为字符集大小)
  • 双端队列优化:O(n) 时间复杂度,O(k) 空间复杂度

3. 常用技巧

  1. 固定窗口初始化let sum = nums.slice(0, k).reduce((a, b) => a + b, 0)
  2. 窗口滑动更新sum += nums[i] - nums[i - k]
  3. 哈希表计数record[char] = (record[char] || 0) + 1
  4. 双指针调整窗口left = Math.max(left, record[char] + 1)

二、核心技术

技巧一:固定窗口滑动

适用场景:寻找长度固定的连续子数组的最大/最小值

核心要点

  • 先计算初始窗口的总和/最大值
  • 每次滑动时仅更新变化的部分,避免重复计算
  • 时间复杂度O(n),空间复杂度O(1)

典型例题子数组最大平均数 I

var findMaxAverage = function(nums, k) {
  let max = -Infinity;
  let sum = 0;
  for (let i = 0; i < k; ++i) {
    sum += nums[i];
  }
  max = sum / k;
  for (let i = k; i < nums.length; ++i) {
    sum += nums[i] - nums[i - k];
    max = Math.max(max, sum / k);
  }
  return max;
};

技巧二:可变窗口滑动(双指针)

适用场景:寻找满足特定条件的最长/最短子串

核心要点

  • 使用左右指针动态调整窗口大小
  • 哈希表记录窗口内字符的最新位置
  • 当遇到重复字符时,移动左指针到重复位置的下一位

典型例题无重复字符的最长子串

var lengthOfLongestSubstring = function(s) {
    let max = 0, left = 0;
    const record = {};
    for (let right = 0; right < s.length; ++right) {
        const curChar = s[right];
        if (curChar in record && record[curChar] >= left) {
            left = record[curChar] + 1;
        }
        record[curChar] = right;
        max = Math.max(max, right - left + 1);
    }
    return max;
};

技巧三:可变窗口滑动+哈希表计数

适用场景:寻找可以通过k次替换得到的最长重复字符子串

核心要点

  • 哈希表记录窗口内各字符的出现次数
  • 维护窗口内出现次数最多的字符的频率
  • 当窗口长度超过maxCount + k时,收缩左边界

典型例题替换后的最长重复字符

var characterReplacement = function(s, k) {
    let max = 0, maxCount = 0, left = 0, right = 0;
    const record = {};
    while (right < s.length) {
        record[s[right]] = (record[s[right]] || 0) + 1;
        maxCount = Math.max(maxCount, record[s[right]]);
        if (maxCount + k < (right - left + 1)) {
            record[s[left]] -= 1;
            ++left;
        }
        max = Math.max(max, right - left + 1);
        ++right;
    }
    return max;
};

技巧四:固定窗口滑动+哈希表计数

适用场景:判断一个字符串是否包含另一个字符串的排列

核心要点

  • 先统计目标字符串的字符频率
  • 滑动窗口时动态更新字符频率
  • 比较窗口内字符频率是否与目标字符串完全匹配

典型例题字符串的排列

var checkInclusion = function(s1, s2) {
    if (s1.length > s2.length) {
        return false;
    }
    const charCount = new Map();
    for (const c of s1) {
        charCount.set(c, (charCount.get(c) || 0) + 1);
    }
    const windowWidth = s1.length;
    for (let i = 0; i <= s2.length - windowWidth; ++i) {
        const charCountCopy = new Map(charCount);
        for (let j = 0; j < windowWidth; ++j) {
            const curChar = s2[j + i];
            if (charCountCopy.has(curChar)) {
                charCountCopy.set(curChar, charCountCopy.get(curChar) - 1);
                if (charCountCopy.get(curChar) === 0) {
                    charCountCopy.delete(curChar);
                }
                continue;
            }
            break;
        }
        if (charCountCopy.size === 0) {
            return true;
        }
    }
    return false;
};

技巧五:双端队列优化滑动窗口

适用场景:寻找滑动窗口中的最大值

核心要点

  • 双端队列存储元素索引,而非元素值
  • 队列保持递减顺序,队首元素始终是当前窗口的最大值
  • 移除队列中不再属于当前窗口的元素

典型例题滑动窗口最大值

// 优化后的解法(原解法超时)
var maxSlidingWindow = function(nums, k) {
    const q = [];
    const result = [];
    for (let i = 0; i < nums.length; i++) {
        // 移除队列中比当前元素小的元素
        while (q.length && nums[i] >= nums[q[q.length - 1]]) {
            q.pop();
        }
        q.push(i);
        // 移除队列中不在当前窗口的元素
        while (q[0] <= i - k) {
            q.shift();
        }
        // 当窗口形成后,记录最大值
        if (i >= k - 1) {
            result.push(nums[q[0]]);
        }
    }
    return result;
};

三、易错点提醒

  1. 初始值设置错误:最大值初始值应设为-Infinity而非0
  2. 窗口边界计算错误:固定窗口滑动时,窗口起始位置应为i - k
  3. 哈希表更新不及时:可变窗口滑动时,忘记更新字符的最新位置
  4. 双指针调整错误:遇到重复字符时,左指针应移动到重复位置的下一位而非当前位置
  5. 超时问题:暴力解法时间复杂度较高,需使用滑动窗口优化

四、学习心得

滑动窗口算法的优势

  1. 时间效率高:将暴力解法的O(n^2)时间复杂度优化为O(n)
  2. 空间效率高:大部分情况下仅需O(1)或O(k)的额外空间
  3. 逻辑清晰:通过窗口的滑动直观地处理连续子数组/子串问题

解题思维模式

  1. 问题分析:确定是固定窗口还是可变窗口问题
  2. 数据结构选择:根据问题选择合适的数据结构(哈希表、双端队列等)
  3. 窗口初始化:设置初始窗口的状态和边界
  4. 窗口滑动:动态调整窗口大小并更新状态
  5. 结果记录:记录满足条件的结果