latency.lab
Algorithms · Topic 03

Sliding Window

Level: foundationTool: incremental window state, not recomputationTime: ~60 min

By the end you can

Sliding window is what two pointers turns into when the "window" itself — not just its edges — is the thing carrying state: a running sum, a count per character, a max. The whole point is the same as two pointers: never redo work you've already paid for. Here that means never recomputing a window's sum/count/max from scratch when you slide it by one.

Two window shapes, and how to tell them apart

  • Fixed size. The window's length is given up front (Average of Subarrays of Size K, Sliding Window Maximum). Sliding it by one always removes exactly one element (the one falling off the left) and adds exactly one (the one entering on the right). The update is O(1) per step regardless of how big the window is.
  • Variable size, grow/shrink. The window's length isn't known in advance — it grows until some condition is violated (or satisfied), then shrinks from the left until it's valid again (Longest Substring Without Repeating Characters, Minimum Window Substring). The right pointer only ever moves forward; the left pointer only ever moves forward. Neither ever resets — that's what keeps the total work O(n) instead of O(n²), even though it looks like a loop inside a loop.
The habit to build

Before coding, ask: "is the window size fixed, or does it depend on the data?" Fixed size means subtract-outgoing/add-incoming. Variable size means "grow until condition breaks, then shrink until it's fixed" — and you must be precise about whether you're shrinking while a condition holds (Minimum Window Substring: shrink while still valid, to find the smallest) or growing until a condition is violated then shrinking one step back (Longest Substring Without Repeating Characters: shrink until valid again, to find the largest).

4
2
7
1
9
window sum = 13
→
4
2
7
1
9
window sum = 10
Fixed-size window (k=3) sliding by one on [4, 2, 7, 1, 9]. The sum updates in O(1): 13 − 4 (left, leaving) + 1 (right, entering) = 10 — no re-adding the 2 and 7 that stayed in the window both times.

Why the naive approach is the trap here

Almost every sliding-window problem has an obvious brute force: try every window (or every substring), check it, keep the best. That's O(n²) at best, sometimes O(n·k) or O(n³) if checking a window itself isn't O(1). The entire skill is recognizing that a window's answer relates to the previous window's answer by only a small, cheap update — so you maintain state incrementally instead of rebuilding it.

incremental-vs-recompute.cpp
// WRONG instinct: recompute the window's sum from scratch every slide -- O(n*k) total.
for (int i = 0; i + k <= n; ++i) {
    int sum = 0;
    for (int j = i; j < i + k; ++j) sum += nums[j];   // re-adds k elements every single time
    best = std::max(best, sum);
}

// RIGHT instinct: the window changed by exactly one element on each side -- O(n) total.
int sum = 0;
for (int i = 0; i < k; ++i) sum += nums[i];         // pay for the first window once
best = sum;
for (int i = k; i < n; ++i) {
    sum += nums[i] - nums[i - k];                      // add what entered, remove what left -- O(1)
    best = std::max(best, sum);
}

Common mistakes

  • Recomputing a count/sum from scratch on every shrink step instead of decrementing incrementally — turns an O(n) solution back into O(n²) while still "looking like" sliding window.
  • Forgetting to remove a map entry once its count hits zero (Fruit Into Baskets, Longest Substring with K Distinct) — a stale zero-count entry still counts toward "how many distinct keys are in the window," silently breaking the distinct-count check.
  • Shrinking with the wrong condition — "shrink while a condition holds" (looking for the shortest valid window, like Minimum Window Substring) is a different loop than "shrink until a condition is restored" (looking for the longest valid window, like Longest Substring Without Repeating Characters). Copy-pasting the wrong shape silently returns the wrong extreme.
  • Decreasing a "best seen so far" tracker when it shouldn't be decreased — Longest Repeating Character Replacement's max_freq is deliberately never decremented on shrink, because a stale-but-too-high value can only ever make the window shrink more conservatively, never accept a wrong answer. Removing that reasoning and "fixing" it to always stay accurate actually makes the sliding window logic more complex for no benefit.
  • Using a hash map where a fixed-size array would do (26 lowercase letters, 256 ASCII values) — correct either way, but the array avoids hashing overhead and is the more "efficient developer" instinct once you know the input's character set is bounded.

The deque trick (Sliding Window Maximum)

Fixed-size windows are usually the easy case — until the thing you need from the window is its max, not its sum. A max can't be updated by just "add the incoming value" the way a sum can, because the max might be the element that's about to fall off the left edge. The fix: maintain a deque of indices, kept in strictly decreasing order of value. Any index whose value is ≤ the incoming value can never be the max again (the new value is both bigger and will outlive it) — so pop it immediately. This keeps every index pushed and popped at most once across the whole run, which is what makes it O(n) instead of O(n log n) (a heap) or O(n·k) (recompute per window).

Takeaways

  • Fixed-size window: sliding by one removes exactly one element and adds exactly one — O(1) per step, subtract-outgoing/add-incoming.
  • Variable-size window: grow the right edge, shrink the left edge when a condition breaks (or to find the tightest window while it still holds) — each pointer moves forward only, giving O(n) total despite the nested-loop look.
  • The recurring bug is silently falling back to recomputation on shrink/grow instead of an incremental update — same class of mistake as the naive brute force you were trying to avoid.
  • When "max of the window" is the thing you need, a monotonic deque of indices (not values) keeps it O(n) — a plain running max doesn't work because the max can expire off the left edge.

Try it: fixed-size window

Average of Subarrays of Size K is the purest version of this shape — the subtract-outgoing/add-incoming update from the walkthrough above, applied directly.

Live

Loading starter code…

Try it: variable-size, grow then shrink

Longest Substring Without Repeating Characters is the canonical grow/shrink problem — track the last seen index of each character, and jump the left edge forward the moment a repeat shows up.

Live

Loading starter code…

Try it: the monotonic deque

Sliding Window Maximum is the one where a plain running value genuinely isn't enough — this is the deque-of-indices technique from the walkthrough above, in full.

Live

Loading starter code…

Keep going

That's 3 of the 12 problems in this topic. Minimum Window Substring and Longest Repeating Character Replacement are worth doing once these three feel automatic — they combine grow/shrink with an extra layer of bookkeeping. See the full Sliding Window set →

Checkpoint · Sliding Window

5 questions · pass at 70%

Q01
A problem asks for the max sum among all windows of a GIVEN fixed length k. What's the O(1)-per-step update as the window slides?
WhyFixed-size windows only ever change by one element on each side per slide — subtract what left, add what entered. Recomputing the whole sum each time is the O(n·k) trap this technique exists to avoid.
Q02
Minimum Window Substring shrinks the left edge while the window STILL satisfies the requirement. Why not shrink until it stops satisfying it, like Longest Substring Without Repeating Characters does?
WhyThe two problems want opposite extremes: the shortest valid window (shrink while still valid, recording the size at each step) vs. the longest valid window (grow until invalid, then shrink back one step). Using the wrong loop shape silently returns the wrong extreme.
Q03
In Fruit Into Baskets, why must a fruit type's entry be removed from the count map entirely once its count reaches zero, instead of just leaving it at zero?
WhyIf distinctness is tracked via the map's size (how many keys exist), a key left behind at count zero is still "present" as far as size() is concerned, breaking the at-most-2-types (or at-most-k) check even though its actual count is zero.
Q04
Longest Repeating Character Replacement tracks max_freq (the most frequent letter's count in the current window) but never decreases it when the window shrinks. Why is that safe?
WhyAn answer of that quality was already achieved at some point, so a stale high max_freq underestimates how many replacements the current window would need — at worst it delays a shrink by a step, it never lets a genuinely invalid window get recorded as the best answer.
Q05
Why does Sliding Window Maximum need a monotonic deque instead of just tracking a running max the way a running sum works for Average of Subarrays?
WhyA sum's incremental update works because you can always undo the exact contribution of a leaving element. A max can't be "undone" that way — if the max leaves the window, you need to already know the next-largest surviving candidate, which is exactly what the decreasing-order deque maintains for free.

Finished Sliding Window?

Pass the quiz to complete it automatically.