滑动窗口与双指针进阶

本节目标

变长滑动窗口:用 left/right 两个指针框住一个窗口,right 扩张、left 收缩,维护窗口内某种约束。

// 运行环境:Node.js 14+
// 保存为 dsa-l10.js,执行:node dsa-l10.js

// 最长无重复字符子串
function lengthOfLongestSubstring(s) {
  const seen = new Set()
  let left = 0, maxLen = 0
  for (let right = 0; right < s.length; right++) {
    while (seen.has(s[right])) {        // 出现重复,左边界收缩
      seen.delete(s[left++])
    }
    seen.add(s[right])                  // 右边界纳入
    maxLen = Math.max(maxLen, right - left + 1)
  }
  return maxLen
}
console.log(lengthOfLongestSubstring('abcabcbb')) // 3

最小覆盖子串(含目标字符的最短窗口):

function minWindow(s, t) {
  const need = new Map()
  for (const ch of t) need.set(ch, (need.get(ch) || 0) + 1)
  let left = 0, formed = 0, minLen = Infinity, start = 0
  for (let right = 0; right < s.length; right++) {
    const ch = s[right]
    if (need.has(ch)) {
      need.set(ch, need.get(ch) - 1)
      if (need.get(ch) === 0) formed++
    }
    while (formed === need.size) {       // 窗口已覆盖全部 t
      if (right - left + 1 < minLen) { minLen = right - left + 1; start = left }
      const L = s[left++]
      if (need.has(L)) { if (need.get(L) === 0) formed--; need.set(L, need.get(L) + 1) }
    }
  }
  return minLen === Infinity ? '' : s.slice(start, start + minLen)
}
console.log(minWindow('ADOBECODEBANC', 'ABC')) // 'BANC'

窗口最大值(单调队列):维护一个递减双端队列,队首永远是窗口最大值。

function maxSlidingWindow(nums, k) {
  const q = [], res = []
  for (let i = 0; i < nums.length; i++) {
    while (q.length && nums[q[q.length - 1]] <= nums[i]) q.pop() // 维护递减
    q.push(i)
    if (q[0] <= i - k) q.shift()                                 // 移出窗口
    if (i >= k - 1) res.push(nums[q[0]])
  }
  return res
}
console.log(maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3)) // [3,3,5,5,6,7]

名词解释

课后练习

  1. 最长无重复子串为什么用 Set 而不是数组?
    • 答案:Set 的 has/delete/add 都是 O(1),判断"右边界字符是否已在区间内"瞬间完成;用数组 indexOf 则要 O(n),整体退化成 O(n²)。
  2. maxSlidingWindow 里 while (nums[q[尾]] <= nums[i]) q.pop() 在做什么?
    • 答案:新元素 nums[i] 进窗口前,先把队尾"比它小且更老"的元素清掉,保证队列递减——这些被清掉的元素既不比 nums[i] 大、又在它左边先出窗,永远没机会当最大值。

总结

滑动窗口是"把暴力 O(n²) 收敛成 O(n)"的又一大利器,核心套路就一句:右指针扩张收集信息,左指针在"约束被破坏"时收缩。它和双指针的区别很微妙——滑动窗口总是框住一段连续区间并维护某个不变量(如无重复、覆盖目标串),而双指针有时只是一个快一个慢各司其职。最难的地方在于"什么时候收缩左边界",这需要你先想清楚窗口的合法条件,再用 while 收缩到刚好合法。单调队列则是窗口类问题的进阶武器:当问题变成"每个窗口的最大值",普通窗口扫描会退化,而递减队列让队首恒为最大值、O(1) 取用。我的建议:滑窗题先写"扩张—收缩—更新答案"三段式骨架,再往里填条件,几乎不会乱。