DSA · Topic 3 of 21

Sliding Window

75 XP

Sliding window is a disciplined way to solve contiguous range problems in linear time by maintaining state incrementally instead of recomputing from scratch.

If you are still recomputing every subarray sum/frequency independently, you are paying O(n^2) for work that should be O(n).


Mental Model

Define a window [left, right] over contiguous elements.

Each step does one or more of:

  1. Expand right -> include new element
  2. Shrink left -> remove old element(s)
  3. Measure answer on current valid window

The central invariant is:

  • every element enters window at most once (when right passes it)
  • every element leaves window at most once (when left passes it)

So total pointer movement is O(n), even if there is a while inside a for.


Two Families You Must Separate

Fixed window (size k)

Window size is constant. Perfect when prompt says:

  • “subarray of size k”
  • “average of every k elements”
  • “maximum sum over length k”

Variable window (size changes)

Window expands and shrinks to satisfy a constraint. Typical prompts:

  • longest substring with at most k distinct chars
  • minimum length subarray with sum >= target
  • count subarrays with at most k odds, etc.

Fixed Window: O(1) Update Formula

Instead of recomputing a k-length window sum in O(k), reuse previous window:

nextWindow = prevWindow + enteringValue - leavingValue

Example: max sum of any subarray of size k

function maxSumK(nums: number[], k: number): number {
  if (k <= 0 || k > nums.length) return -Infinity

  let windowSum = 0
  for (let i = 0; i < k; i++) windowSum += nums[i]

  let best = windowSum
  for (let right = k; right < nums.length; right++) {
    windowSum += nums[right] - nums[right - k]
    best = Math.max(best, windowSum)
  }
  return best
}

Common fixed-window variants

  • max average subarray I
  • number of substrings of length k satisfying condition
  • rolling hash / rolling checksum style problems

Variable Window: Valid/Invalid Invariant

For variable windows, define a predicate:

  • isValid(windowState) == true means current window is acceptable

Then:

  1. expand right by one
  2. while invalid, shrink left
  3. update answer (for longest-valid style) after restored validity

Generic template (at-most constraints)

let left = 0
let answer = 0
const state = new Map<string, number>() // adapt as needed

for (let right = 0; right < arr.length; right++) {
  // add arr[right] to state

  while (/* window invalid */) {
    // remove arr[left] from state
    left++
  }

  // window valid here
  answer = Math.max(answer, right - left + 1)
}

This pattern powers a huge fraction of medium interview string/array questions.


Example 1: Longest Substring Without Repeating Characters

function lengthOfLongestSubstring(s: string): number {
  const lastSeen = new Map<string, number>()
  let left = 0
  let best = 0

  for (let right = 0; right < s.length; right++) {
    const ch = s[right]
    if (lastSeen.has(ch) && lastSeen.get(ch)! >= left) {
      left = lastSeen.get(ch)! + 1
    }
    lastSeen.set(ch, right)
    best = Math.max(best, right - left + 1)
  }
  return best
}

Why this jump-left version is strong

Instead of shrinking one step at a time, we jump left directly after duplicate’s previous index. Still O(n), cleaner constant factors.


Example 2: Minimum Size Subarray Sum (Positive Numbers)

function minSubarrayLen(target: number, nums: number[]): number {
  let left = 0
  let sum = 0
  let best = Infinity

  for (let right = 0; right < nums.length; right++) {
    sum += nums[right]

    while (sum >= target) {
      best = Math.min(best, right - left + 1)
      sum -= nums[left]
      left++
    }
  }
  return best === Infinity ? 0 : best
}

Critical caveat

This works because numbers are non-negative.
If negatives exist, shrinking can increase sum unpredictably and this invariant breaks. Use prefix sum + deque / binary search variants instead depending on problem.


Exact, At-Most, At-Least Windows

Most variable-window problems are one of these forms:

A) Longest with at most K violations

  • keep window valid with while invalid -> shrink
  • update max length

B) Minimum with at least target

  • expand until valid
  • then shrink aggressively while still valid
  • update min length during shrink

C) Count of exactly K

Compute:

countExactly(K) = countAtMost(K) - countAtMost(K - 1)

This transform is interview gold for counting subarrays/substrings.


Example 3: Count Subarrays with Exactly K Odd Numbers

function atMostKOdds(nums: number[], k: number): number {
  let left = 0
  let odd = 0
  let count = 0

  for (let right = 0; right < nums.length; right++) {
    if (nums[right] % 2 !== 0) odd++

    while (odd > k) {
      if (nums[left] % 2 !== 0) odd--
      left++
    }

    count += right - left + 1 // all windows ending at right
  }
  return count
}

function numberOfSubarraysExactlyKOdds(nums: number[], k: number): number {
  return atMostKOdds(nums, k) - atMostKOdds(nums, k - 1)
}

Sliding Window Maximum (Monotonic Deque Bridge)

Classic hard problem where plain sum/frequency window state is not enough.

Need max of each size-k window in O(n), not O(nk).

Store candidate indices in decreasing-value order:

function maxSlidingWindow(nums: number[], k: number): number[] {
  const result: number[] = []
  const deque: number[] = [] // stores indices, values decreasing
  let head = 0 // logical front pointer (avoids O(n) shift)

  for (let i = 0; i < nums.length; i++) {
    // remove out-of-window indices
    if (head < deque.length && deque[head] <= i - k) head++

    // maintain decreasing deque
    while (head < deque.length && nums[deque[deque.length - 1]] <= nums[i]) {
      deque.pop()
    }

    deque.push(i)
    if (i >= k - 1) result.push(nums[deque[head]])

    // occasional compaction so deque doesn't grow unbounded from stale prefix
    if (head > 1024 && head * 2 > deque.length) {
      deque.splice(0, head)
      head = 0
    }
  }

  return result
}

Production note: this head-pointer pattern preserves O(n) behavior in JavaScript as well.


Decision Framework: Sliding Window vs Something Else

Use sliding window when all are true:

  1. data is linear (array/string)
  2. target segment is contiguous
  3. window validity/score can be incrementally updated

Do not force sliding window when:

  • subsequence (non-contiguous) problems -> DP/greedy
  • arbitrary range queries repeated many times -> prefix sums/segment trees
  • constraints are non-monotonic under shrink/expand

Common Bugs Checklist

  1. Updating answer before restoring validity
  2. Using if instead of while for shrink phase
  3. Forgetting to decrement state when moving left
  4. Wrong window length formula (right - left + 1)
  5. Mishandling empty input / k > n / k == 0
  6. Assuming positive numbers where negatives are allowed
  7. Frequency map cleanup mistakes (count drops to 0 but key not removed when needed)

Interview Communication Template

Speak like this:

  1. “I maintain a window [left, right] and a state object.”
  2. “After adding right, I shrink until invariant X is restored.”
  3. “Each index is added and removed at most once, so O(n).”

This communicates correctness, not just coding speed.


Complexity Summary

VariantTimeSpaceNotes
Fixed sizeO(n)O(1)O(1) update per slide
Variable with map/setO(n)O(k) to O(sigma)depends on tracked state
Monotonic deque window maxO(n)O(k)each index pushed/popped once

Why still O(n) with nested loops:

  • outer right increments n times
  • inner shrink/deque pops cumulatively at most n times

Total O(2n) style accounting.


Easy

  • Maximum Sum Subarray of Size K
  • Longest Substring Without Repeating Characters

Medium

  • Longest Repeating Character Replacement
  • Permutation in String
  • Minimum Size Subarray Sum
  • Fruit Into Baskets (at most 2 distinct)

Hard / high-signal

  • Minimum Window Substring
  • Sliding Window Maximum
  • Subarrays with K Different Integers (exactly K via at-most trick)

Final Takeaway

Sliding window is an invariant-maintenance technique, not a memorized snippet.

Define:

  1. what state your window tracks
  2. what “valid” means
  3. when to update answer
  4. why pointer movement is monotonic

Do this consistently and most contiguous optimization/counting problems become predictable and linear.