🧩 Coding Interviews · ch.06 · sliding window
🧩 Part II · Linear Structures · chapter 6 / 24

Grow the right edge,
shrink when it breaks

A window is two indexes and a promise: everything between them satisfies the rule. Keep the promise while both edges only ever move right, and "longest contiguous anything" collapses from O(n²) candidates to one O(n) pass.

Chapter 5's two pointers walked toward each other and met in the middle. This chapter's two pointers walk the same direction, dragging a stretch of the array between them like a caterpillar — front end inches forward, back end catches up only when it must. That one picture solves everything the bank phrases as "longest/shortest substring or subarray such that…".

The plan: why contiguity licenses the trick, the grow/shrink loop and its invariant, the flagship no-repeats problem, fixed-width windows, the degenerate one-number window hiding inside Best Time to Buy & Sell Stock, the "shortest" mirror image, and then you take the wheel and try not to crash the window yourself.

1Quadratic candidates, linear pass — the caterpillar

Count the subarrays of an n-element array: one for every (start, end) pair — about n²/2 of them. The brute force checks each candidate, and checking usually costs O(n) itself, so you're staring at O(n³), or O(n²) with mild cleverness. For n ≈ 10⁵ (Chapter 3's budget math), both are dead on arrival.

The rescue comes from a fact so obvious it's easy to miss: consecutive candidates overlap almost completely. The subarray from index 2 to 9 and the subarray from 2 to 10 share eight elements. Recomputing from scratch throws that overlap away; a window keeps it. Slide the right edge one step and you've changed the candidate by exactly one element in and, sometimes, a few elements out. Update, don't rebuild.

Hence the caterpillar: two indexes l and r, and some cheap running summary of what's between them — a sum, a count map, a set. Both indexes only ever move right. That's the whole engine.

💡
Why it's O(n) and not O(n²): the inner shrink loop looks nested, but l can only move right, and it can never pass r. Each index enters the window once and leaves at most once — at most 2n pointer moves total, ever. O(n) — one pass, amortized. Say this sentence in the interview; it's the part they're listening for.

2Grow right, shrink only when forced — the invariant

Every variable-width window problem is the same loop wearing different state. Pick an invariant — a property the window must satisfy ("no repeated characters", "sum ≤ S", "at most k zeros flipped"). Then:

  • Grow greedily. Every turn, push r one step right and absorb the new element into the window state.
  • Shrink only when broken. If the new element broke the invariant, advance l — evicting elements from the state — until the invariant holds again. Not one step further.
  • Record at the legal moments. For a longest problem, the window is at its best right after the shrink loop restores legality: best = max(best, r − l + 1).

The discipline is the pattern. Shrink too eagerly and you throw away the very window you were hunting; grow while broken and your "best" measurements are lies. Section 7's widget will happily flag you for both.

🎯
The tell: "longest/shortest substring or subarray that satisfies X" — the word contiguous (or its aliases substring/subarray) is the flare. If the problem says subsequence — elements that keep order but needn't touch — the window is powerless and you're headed to DP in Chapter 21. One word, two different chapters.

3No repeats allowed — the flagship problem

Longest Substring Without Repeating Characters Medium is the pattern's poster child. Invariant: every character in the window appears exactly once. State: the set of characters currently inside. The loop: try to grow; if the incoming character is already in the set, the invariant would break — so evict from the left until that older copy is gone, then grow.

Watch it run. The elastic band is the window; the braces below are its state; the green bracket marks the best window found so far. Notice the caterpillar rhythm — long greedy stretches of growth, short bursts of eviction — and notice that neither pointer ever backs up.

Interactive · window animator Longest substring without repeats — play, pause, or step
window [l, r]
best length
0
best substring
pointer moves
0

The widget shrinks one step at a time because that's the honest caterpillar. The production refinement — the one in Section 9's code — remembers each character's last seen index and jumps l straight past the stale copy in one assignment. Same invariant, same O(n), one fewer loop. Mention both in the room; the jump is a nice flex, the one-at-a-time version is easier to get right under pressure.

4When the width is handed to you — fixed windows

Sometimes the problem fixes the width and the elastic goes rigid: "maximum sum subarray of size k", or Permutation in String Medium — does any window of |s1| consecutive characters of s2 use exactly s1's letters? A fixed window is the easy case of the loop: every slide is add one element on the right, drop one on the left, both O(1) against the running state. No shrink loop at all — the widths never disagree.

For Permutation in String the state is a count map (Chapter 4's frequency counter, moonlighting). Keep a single integer matches — how many of the 26 letters currently have the correct count — and each slide touches at most two letters, so the check "is this window an anagram?" is matches == 26, O(1). Total: O(n) with a 26-entry map. The elegant part is what you don't do:

⚠️
The rebuild trap: the classic way to botch a fixed window is recomputing the whole state per slide — re-counting k characters at every position for O(n·k), or worse, sorting each window for O(n·k log k). If your window code contains an inner loop over the window's contents, you've quietly re-invented the brute force. Every slide must be one in, one out.

One fixed-width problem deserves a flag for later: Sliding Window Maximum Hard asks for the max of each window — and a max is the one summary you can't cheaply update when its element leaves. The fix is a monotonic deque that discards dominated elements, and it belongs to Chapter 8's monotonic-stack family. Park it; we'll collect it there.

🎯
The tell: "window/subarray of size k" → fixed window, add-one-drop-one. "Does s2 contain a permutation/anagram of s1" → fixed window of width |s1| plus a count map. The width being named in the statement is the giveaway that the shrink loop disappears.

5The one-number window — Best Time to Buy & Sell Stock

Best Time to Buy & Sell Stock Easy: one buy, one later sell, maximize profit. It doesn't look like a window problem — until you ask what the best trade ending today is: today's price minus the cheapest price seen so far. The right edge is today; the left edge is wherever that minimum lives; and the entire window state has collapsed to a single number, min_so_far. One pass, two variables, O(n) — the caterpillar on a diet.

Scan the chart. The ochre staircase is min_so_far ratcheting downward; the green band is the best trade found so far. Watch the band jump when a new peak clears an old valley.

Interactive · buy-sell profit tracker min-so-far staircase + best-profit band, day by day
day scanned
min so far (buy candidate)
best profit
0
best trade
💡
The degenerate-window move generalizes: whenever "best pair (i, j) with i < j" only needs a summary of everything left of j (the min, the max, a running sum), the left half of the window compresses into one variable. Maximum Subarray runs the same trick with a running sum — that's Kadane, and greedy Chapter 19 claims it.

6Shortest flips the loop — Minimum Window Substring

Minimum Window Substring Hard: the shortest window of s containing every character of t, multiplicity included. Same caterpillar, mirrored logic. For longest problems the window lives in a legal state and shrinking is an emergency. For shortest problems, being legal ("window covers all of t") is the goal, so the roles flip:

  • Grow until valid. Push r right, counting characters, until the window covers t.
  • Shrink while valid. Now every element you can shed makes the answer better — pull l right, recording the width at each still-valid stop, until the window stops covering t.
  • Repeat. Grow to re-validate, shrink to re-tighten, to the end of s.

The state is two count maps and — same trick as Section 4 — a single integer have counting how many distinct characters currently meet their required count, so "is the window valid?" is have == need, O(1) per step. Total O(|s| + |t|). Candidates are recorded only inside the shrink phase, at the tightest moments — never while growing, when the window is carrying slack by construction.

🎯
The tell: "minimum window/substring containing all of…" → grow-until-valid, shrink-while-valid. Any "shortest contiguous X that covers/reaches Y" phrasing is this mirror image — if you catch yourself recording candidates during the grow phase, you've got the loop backwards.

7Take the wheel — the two ways to crash a window

There are exactly two ways to drive a window off the road, and interviewers see both weekly:

  • The illegal grow — extending r while the invariant is already broken. Your window is now measuring garbage, and any "best" you record is fiction.
  • The needless shrink — retreating l while the window is perfectly legal. Nothing false happens; you just silently discard the longest window you'll never see. This is the sneaky one, because the code still runs.

Drive it yourself: the invariant here is window sum ≤ 11, and the goal is the longest legal window. Grow and shrink by hand and the widget flags every infraction; or press Autopilot to watch the canonical loop drive it clean. The bar for a perfect run: match the optimal length with zero flags.

Interactive · invariant breaker Longest window with sum ≤ 11 — you drive; flags for bad moves
window sum / limit
0 / 11
best found / optimal
0 / —
needless shrinks
0
illegal grows (blocked)
0

Notice what the autopilot never does: it never wonders whether to grow. Growth is unconditional; only the shrink is conditional. If your hand-rolled window code has an if deciding whether to advance r, you've usually smuggled in a bug.

8Where the window ends — borders, and the skeleton

The pattern has sharp borders, and knowing them is worth as much as knowing the loop:

  • Negative numbers break sum windows. The shrink logic assumes growing can only push the sum up and shrinking can only pull it down — monotone state. With negatives, "subarray sum equals k" can't be windowed; it's the prefix-sum + hash-map combo, and it's Chapter 7's flagship. Adjacent chapter, one week of confusion saved.
  • "Subsequence" is DP's word. Non-contiguous means no window. Longest Increasing Subsequence lives in Chapter 20, Longest Common Subsequence in Chapter 21.
  • Un-updatable summaries need help. If the state can't cheaply forget a departing element (a max, a median), the plain window stalls — reach for the monotonic deque (Chapter 8) or a heap (Chapter 14).
  • "Exactly k" hides behind "at most". Windows count "at most k" naturally; get "exactly k" as atMost(k) − atMost(k−1) — two clean passes beat one contorted one.

The chapter's canon, in climbing order: Best Time to Buy & Sell Stock Easy, Longest Substring Without Repeating Characters Medium, Permutation in String Medium, Minimum Window Substring Hard, and Sliding Window Maximum Hard once Chapter 8 hands you the deque.

The pattern, as a whiteboard skeleton:

  1. 1Confirm contiguity. Substring/subarray → proceed. Subsequence → this is DP (ch. 20–21); put the marker down.
  2. 2Name the invariant out loud: what must be true of the window? ("no repeats", "sum ≤ S", "covers t").
  3. 3Pick state updatable in O(1) both ways — running sum, count map, set — for both an element entering and one leaving.
  4. 4Init l = 0, empty state, best.
  5. 5For each r: absorb s[r] into the state — growth is unconditional.
  6. 6While the invariant is broken: evict s[l], advance l. (Shortest problems flip it: while valid, record then evict.)
  7. 7Record at the legal moment — longest: after the shrink loop; shortest: inside it.
  8. 8Test the edges: empty input, all-identical characters, window never valid, k larger than n. Then say "O(n) — each index enters once, leaves once."

9Every index enters once, leaves once — the flagship in code

Longest Substring Without Repeating Characters Medium, both ways it appears on real whiteboards. First the canonical caterpillar — a set for state, an explicit shrink loop, exactly Section 2's skeleton. Then the refinement from Section 3: remember each character's last index and let l jump past the stale copy in one move. The Scala version wears the second form as a fold, because a window is secretly an accumulator: the state (l, best, lastSeen) threads through the string once.

⚠️
The stale-copy trap: in the jump version, the guard last[ch] >= l is load-bearing. A character's last appearance may sit behind the current window — already evicted — and jumping l to it would move the left edge backwards, breaking the once-in-once-out argument and the answer with it. Interviewers plant strings like "abba" precisely to spring this.