πŸͺŸ Sliding Window

A learning sheet β€” turn O(nΒ²) subarray scans into one linear pass by reusing the window you already built.

🧠 1 · How to think (and invent it)

The one idea: a contiguous sub-array/sub-string answer depends on a window [left, right]. Instead of rebuilding every window from scratch (O(nΒ²)), slide: when right moves one step, you only add one element; when you must shrink, left only ever moves forward. Each index enters and leaves the window once β‡’ O(n).

The whole topic is one question: "when is my window valid, and which end do I move to fix it?" Answer that with an invariant and the template writes itself.

a b c a b c left right the live window right++β†’ add one element left++β†’ only when forced left never goes backward β†’ O(n)
The window breathes: right always advances; left only catches up when validity demands it.
Mental checklist (1) what does the window state track (a sum? a frequency map? a distinct count?) (2) what makes it invalid? (3) when invalid, you shrink from left; when do you record the answer β€” while valid, or after restoring validity? That last choice is the whole difference between "longest" and "shortest."

Watch it get invented β€” Longest Substring Without Repeating Characters

  1. Start brute. Check every substring for uniqueness β†’ O(nΒ²) or worse.
  2. Spot the waste. If [left, right] has no repeat and you push right, you don't re-scan the whole thing β€” you only need to know if the new char already sits in the window. And when it does, sliding left backward is pointless: any earlier start contains the duplicate too. left only moves forward.
  3. The leap. Keep lastSeen[char]. On each right, jump left to max(left, lastSeen[c] + 1) β€” past the previous copy. Record right βˆ’ left + 1.
  4. Payoff. Each index is visited by right once and by left once β‡’ O(n).
rightcharlastSeen[c] β‰₯ left?leftwindowmaxLen
2cβ€”0abc3
3ayes (0)1bca3
6byes (4)5cb3

Input "abcabcbb" β†’ answer 3. Watch left jump forward on each repeat, never back.

🎯 2 · Attack plan

AskIf yes β†’Pattern
Contiguous subarray/substring? (not a subsequence)sliding window is in playβ€”
Window size is given (k)?add new, drop the one that fell offFixed (Β§4.1)
Find the longest window satisfying a constraint?grow; shrink only while invalid; record afterVariable Β· longest (Β§4.2)
Find the shortest window satisfying a constraint?grow until valid; shrink while valid; record duringVariable Β· shortest (Β§4.3)
Count subarrays with an exact property?exactly(K) = atMost(K) βˆ’ atMost(Kβˆ’1)Counting (Β§4.4)
Need the max/min within each window?monotonic deque of indicesMonotonic deque (Β§4.5)
When sliding window is the WRONG tool Β· The answer is a subsequence, not contiguous β€” that's usually DP, not a window.
Β· Negative numbers break the monotonicity you're relying on: "shortest subarray with sum β‰₯ K" with negatives cannot use a naive window (growing doesn't only-increase the sum) β†’ use prefix sums + a monotonic deque.
Β· Validity isn't monotonic β€” if adding an element can turn an invalid window valid again, the "shrink once" logic is unsafe.

πŸ”Ž 3 Β· Recognition map

Words in the promptReach for
"subarray/substring of size k", "every window of length k", "average of k"Fixed Β§4.1
"longest/maximum substring with…", "no repeating", "at most k distinct", "≀ k replacements/flips"Longest Β§4.2
"shortest/minimum window/subarray that contains… / sums to β‰₯…"Shortest Β§4.3
"number of subarrays with exactly k distinct / sum = goal / k odds"Counting Β§4.4
"maximum/minimum in each window", "sliding window maximum"Deque Β§4.5

🧩 4 · The five core patterns

Each = a one-line model, a πŸ”‘ invariant, a template, a picture, and a 🎀 interview lens. The two variable templates differ in one line β€” where you record the answer.

4.1 Β· Fixed window β€” size k

β€œAdd the newcomer, drop the one that fell off the back.”

Setup: maintain a running window state for exactly k elements. Each step is O(1): add a[right], and once the window is full, subtract a[rightβˆ’k].

2 1 5 1 3 2 drop add k = 3 Β· slide right
The window never re-sums from scratch β€” one subtract, one add per step.
πŸ”‘ Invariant β€” why it's safe After processing index right, the window state describes exactly a[rightβˆ’k+1 .. right]. Because you remove precisely the element leaving and add the one entering, the state is always a faithful summary of the current k-window.
// Maximum sum of any size-k subarray
function maxSumK(a: number[], k: number): number {
  let sum = 0, best = -Infinity;
  for (let right = 0; right < a.length; right++) {
    sum += a[right];                 // add newcomer
    if (right >= k) sum -= a[right - k];  // drop the one that fell off
    if (right >= k - 1) best = Math.max(best, sum); // window is full
  }
  return best;
}
🎀 Interview lens Probed: each slide must be O(1) β€” no re-summing the window. Β· Escalation: sum β†’ average β†’ "permutation/anagram in string" (a fixed window with a 26-length frequency match) β†’ "first negative in every window" (a queue of negatives).

4.2 Β· Variable window β€” longest valid

β€œGrow greedily; shrink only while broken; measure after.”

Setup: expand right every step. If the window became invalid, shrink from left until it's valid again β€” then record the length.

right++always β€” try to grow the window while invalid:left++ (remove a[left] from state) best = max(best, rightβˆ’left+1)window is valid here
Record after restoring validity β€” the window is guaranteed valid at the bottom of the loop.
πŸ”‘ Invariant β€” why it's safe At the end of each iteration, [left, right] is the longest valid window ending at right. Since left only moves forward, a window that became invalid can't be fixed by an earlier left β€” so we never miss a longer valid window.
// Longest substring with at most K distinct characters
function longestKDistinct(s: string, k: number): number {
  const count = new Map<string, number>();
  let left = 0, best = 0;
  for (let right = 0; right < s.length; right++) {
    count.set(s[right], (count.get(s[right]) ?? 0) + 1);  // grow
    while (count.size > k) {                               // shrink WHILE invalid
      const c = s[left++];
      const n = count.get(c)! - 1;
      if (n === 0) count.delete(c); else count.set(c, n);
    }
    best = Math.max(best, right - left + 1);               // record (now valid)
  }
  return best;
}
The famous trap β€” Longest Repeating Character Replacement Window is valid when windowLen βˆ’ maxFreq ≀ k. The intentional bug everyone questions: maxFreq is never decreased on shrink. It can go stale β€” and that's fine, because a longer answer needs a larger maxFreq anyway, so a stale value never lets the window grow falsely.
🎀 Interview lens Probed: why does left never move backward (prove the amortized O(n))? Β· Escalation: no-repeat β†’ at-most-K-distinct β†’ "Max Consecutive Ones III" (≀ k zeros) β†’ "Longest Repeating Char Replacement" (the stale-maxFreq argument above).

4.3 Β· Variable window β€” shortest valid

β€œGrow until valid; then squeeze as much as it stays valid.”

Setup: mirror of 4.2. Expand until the window is valid; then shrink from left while still valid, recording the length during the shrink.

πŸ”‘ Invariant β€” why it's safe Every time the window first becomes valid, the inner loop shrinks it to the shortest valid window ending at (or before) right before right moves again. Recording inside that loop catches every minimal window.
// Minimum Size Subarray Sum β‰₯ target  (positives only)
function minSubArrayLen(target: number, a: number[]): number {
  let left = 0, sum = 0, best = Infinity;
  for (let right = 0; right < a.length; right++) {
    sum += a[right];
    while (sum >= target) {                       // shrink WHILE valid
      best = Math.min(best, right - left + 1);    // record DURING
      sum -= a[left++];
    }
  }
  return best === Infinity ? 0 : best;
}

The one-line difference vs Β§4.2: longest shrinks while invalid and records after; shortest shrinks while valid and records during. Same skeleton, opposite trigger.

🎀 Interview lens Probed: when exactly do you record, and how do you report "no valid window"? Β· Escalation: min subarray sum β†’ "Minimum Window Substring" (a need frequency map + a formed counter that tracks how many required chars are fully satisfied).

4.4 Β· Counting β€” "exactly K" via "at most K"

β€œYou can't window β€˜exactly’; window β€˜at most’ twice and subtract.”

Why: "exactly K" isn't monotonic (a valid window can become invalid and valid again as you grow), so a single window can't count it. But "at most K" is monotonic. So: exactly(K) = atMost(K) βˆ’ atMost(Kβˆ’1).

πŸ”‘ Invariant β€” the counting trick In an atMost window [left, right] that's valid, every subarray ending at right and starting in [left, right] is also valid. That's right βˆ’ left + 1 new subarrays β€” add it each step.
// Subarrays with exactly K distinct integers
function subarraysKDistinct(a: number[], k: number): number {
  const atMost = (k: number): number => {
    if (k < 0) return 0;
    const count = new Map<number, number>();
    let left = 0, res = 0;
    for (let right = 0; right < a.length; right++) {
      count.set(a[right], (count.get(a[right]) ?? 0) + 1);
      while (count.size > k) {
        const n = count.get(a[left])! - 1;
        if (n === 0) count.delete(a[left]); else count.set(a[left], n);
        left++;
      }
      res += right - left + 1;      // # of valid subarrays ending at right
    }
    return res;
  };
  return atMost(k) - atMost(k - 1);
}
🎀 Interview lens Probed: derive exactly = atMost(K) βˆ’ atMost(Kβˆ’1), and justify res += rightβˆ’left+1. Β· Escalation: K distinct β†’ "Binary Subarrays With Sum = goal" β†’ "Count Number of Nice Subarrays" (exactly k odd numbers; same trick on parity).

4.5 Β· Monotonic deque β€” max/min in each window

β€œKeep a deque of candidates in decreasing order; the front is the answer.”

Setup: store indices in a deque whose values are strictly decreasing. The front is always the window max. Pop the back while it's ≀ the newcomer (it can never be the max again); pop the front when it slides out of the window.

values entering β†’ 8 5 3 front = max back: popped when ≀ newcomer deque: indices, values ↓
Each index is pushed once and popped once β†’ amortized O(n) for all windows combined.
πŸ”‘ Invariant β€” why it's safe The deque holds, in order, the indices that could still be the max of some future window β€” strictly decreasing by value, all inside [rightβˆ’k+1, right]. A smaller-or-equal value behind a newer larger one is dominated forever, so dropping it loses nothing.
// Sliding Window Maximum β€” max of every size-k window
function maxSlidingWindow(a: number[], k: number): number[] {
  const dq: number[] = [];   // indices; a[dq] strictly decreasing
  let head = 0;               // ⭐ head pointer β€” never shift(), O(1) evict
  const res: number[] = [];
  for (let i = 0; i < a.length; i++) {
    while (dq.length > head && a[dq[dq.length - 1]] <= a[i]) dq.pop();  // keep decreasing
    dq.push(i);
    if (dq[head] <= i - k) head++;          // evict out-of-window front β€” O(1), no shift
    if (i >= k - 1) res.push(a[dq[head]]);  // front = current max
  }
  return res;
}
🎀 Interview lens Probed: prove amortized O(n) (each index pushed/popped once), and why Array.shift() would silently make this O(nΒ²) β€” a head pointer keeps front-eviction O(1) since this deque only ever pops from the back and evicts from the front, never inserts at the front. Β· Escalation: max β†’ min (mirror the comparison) β†’ "Longest subarray with abs(maxβˆ’min) ≀ limit" (run two deques at once).

⏱️ 5 · Complexity

PatternTimeSpaceNote
Fixed windowO(n)O(1) / O(Οƒ)Οƒ = alphabet if a frequency map is needed
Variable β€” longest / shortestO(n)O(Οƒ)left + right each traverse ≀ n
Counting (at-most Γ—2)O(n)O(Οƒ)two linear passes, still linear
Monotonic dequeO(n)O(k)each index pushed/popped once
The amortized argument right advances n times; left advances at most n times total across the whole run (never resets). So even though there's an inner while, the combined work is O(n) β€” say this explicitly, don't let the nested loop scare you into O(nΒ²).

⚠️ 6 Β· Pitfalls β€” and the test that catches each

BugThe input that exposes it
Shrinking with if instead of while (longest) β€” one removal may not restore validity."aaab", k=1 distinct β†’ an if leaves 2 distinct in the window.
Recording the answer at the wrong moment (longest = after; shortest = during).Min-window on [2,3,1,2,4,3], target 7 β†’ recording after the shrink loop misses length 2 ([4,3]).
Off-by-one window length (right βˆ’ left vs + 1).Any single-element answer, e.g. target = a[i] exactly β†’ should report length 1.
Fixed window: forgetting to subtract the element that left.[1,1,1,1], k=2 β†’ window sum keeps growing past 2 elements.
Using a naive window with negative numbers.[2,-1,2], target 3 β†’ growing doesn't monotonically raise the sum; misses / mis-shrinks.
Deque stores values, not indices β€” can't evict by position.Any window where the max repeats, e.g. [5,3,5], k=2.
Frequency map keeps zero entries β†’ size overcounts distinct.at-most-k on "aba", k=1 β†’ stale a:0 inflates size.
πŸ”— This pattern, elsewhere Sliding window is a same-direction two-pointer β€” it's the Read/Write cousin from two-pointers.html with a shrink rule (and the converging vs. window distinction is sorting-vs-contiguity). Fixed windows are often replaceable by prefix sums. The monotonic deque is the monotonic-stack idea applied to a moving range β€” a stack open at both ends. Negative-number variant ("Shortest Subarray with Sum β‰₯ K") needs prefix sums + a monotonic deque β€” the bridge from this sheet to that one.
βž• Beyond the window β€” Subarray Sum = K (560) Β· Contains Duplicate II (219) Β· Min Window Subsequence (727)
560 is the "why sliding window fails" question: with negative numbers a longer window does not mean a bigger sum, so there is no shrink rule β€” use prefix sums + a hash map instead. 219 is the fixed-window-with-a-set warm-up. 727 looks like 76, but order matters (subsequence), so there is no count to shrink on: it is 2D string DP, see the DP sheet.
// 560 Subarray Sum Equals K β€” NEGATIVES break the window β†’ prefix sums + counts
function subarraySum(nums: number[], k: number): number {
  const freq = new Map<number, number>([[0, 1]]);        // empty prefix, so subarrays starting at 0 count
  let sum = 0, count = 0;
  for (const x of nums) {
    sum += x;
    count += freq.get(sum - k) ?? 0;                     // earlier prefixes that leave exactly k
    freq.set(sum, (freq.get(sum) ?? 0) + 1);             // insert AFTER the lookup
  }
  return count;
}

// 219 Contains Duplicate II β€” fixed window of the last k values
function containsNearbyDuplicate(nums: number[], k: number): boolean {
  const win = new Set<number>();
  for (let i = 0; i < nums.length; i++) {
    if (win.has(nums[i])) return true;
    win.add(nums[i]);
    if (win.size > k) win.delete(nums[i - k]);           // evict AFTER adding
  }
  return false;
}
⚠️ Traps 560: seed the map with {0: 1} and look up before inserting the current prefix. 219: the window holds at most k elements.

πŸ“Š Coverage β€” pattern β†’ LeetCode (medium & hard)

Own each pattern and these are in range. Bold = Hard tier.

PatternRepresentative LeetCodeTier
Fixed window643 Max Average Β· 567 Permutation in String Β· 438 Find All Anagrams Β· 219 Contains Duplicate IIMed
Longest variable3 Longest Substring No Repeat Β· 424 Char Replacement Β· 340 K DistinctMed
Shortest variable209 Min Size Subarray Sum Β· 76 Minimum Window SubstringMed β†’ Hard
Exactly-K (atMost trick)930 Binary Subarrays Β· 992 Subarrays w/ K Distinct Β· 1248 Β· 560 Subarray Sum = K (negatives β†’ prefix sums)Med β†’ Hard
Monotonic deque239 Sliding Window Maximum Β· 1438 Limit ≀ abs diffMed β†’ Hard

βœ… 7 Β· Self-check

Cover the right column (blurred β€” hover/tap to reveal). Say your answer out loud first.

PromptAnswer
What is the one-line difference between the longest and shortest variable templates?Longest shrinks while invalid and records after; shortest shrinks while valid and records during.
Why is the whole scan O(n) despite a nested while?left only moves forward and never resets, so it advances ≀ n times total across the run. Amortized linear.
How do you count subarrays with exactly K distinct?atMost(K) βˆ’ atMost(Kβˆ’1). "Exactly" isn't monotonic; "at most" is.
In an atMost window, why add right βˆ’ left + 1?Every subarray ending at right and starting anywhere in [left,right] is also valid β€” that's how many new ones end at right.
Why does the monotonic deque store indices, not values?To evict the front when it slides out of the window (a position check) β€” values alone can't tell you that.
When does sliding window fail?Negative numbers (sum not monotonic) or non-contiguous (subsequence) problems; the shrink-once invariant breaks.
Longest Repeating Char Replacement: why is the stale maxFreq not a bug?A longer answer needs a larger maxFreq; a stale (too-small) value can only under-grow the window, never falsely grow it.
Why can't you use a sliding window for Subarray Sum Equals K?Negative numbers: extending the window can lower the sum, so shrinking has no monotone rule. Use prefix sums and count earlier prefixes equal to sum βˆ’ k.
Min Window Subsequence β€” why scan backward after the forward pass?The forward pass fixes the earliest end; walking backward matching t in reverse finds the tightest start for that end.

Write these cold (no peeking)

For each: write the validity condition and where you record the answer as a comment first, then the loop.