skip to content

Why is a grow-right/shrink-left sliding window O(n) overall when a shrink while-loop sits inside the main loop?

level: middleimportance: must knowfreq 65%

answer

  1. count pointer moves, not loop iterations
  2. does the left pointer ever go backward?
  3. each element enters once, leaves at most once
  4. two pointers, n positions each: 2n moves total

basics

~20 s

Because both pointers only move forward. The right pointer takes at most n steps and the left pointer at most n steps across the entire scan, so total work is bounded by about 2n pointer moves — O(n).

solid answer

~40 s

The nested-loop shape is misleading; charge the work to pointer movements instead of loop levels. The outer loop advances the right pointer exactly once per iteration — at most `n` moves. Every iteration of the inner while advances the left pointer, which never moves backward, so it also moves at most `n` times over the whole scan. Total pointer movements are bounded by `2n` no matter how they cluster: one iteration may evict twenty elements, but those twenty are gone and can never be evicted again. This is an amortized, aggregate argument — no single outer iteration is guaranteed O(1); only the sum over the run is O(n). Multiply by the per-move update cost, typically O(1) expected, to get O(n) expected overall.

go deeper

for a junior

Be ready to state that the technique is linear and give the one-sentence reason: both pointers only ever move forward over the sequence.

for a middle

An interviewer expects the full aggregate argument — bound total pointer movements by 2n, admit that no single step is guaranteed O(1), and use 'amortized' correctly, distinguishing it from average-case analysis.

for a senior

Be ready to spot the secretly quadratic variants in review — from-scratch recomputation per move, backtracking left pointers — and to restate the bound honestly when per-move updates cost more than O(1).

for a principal

Own the explanation itself: defending an amortized bound against 'but I see nested loops' pushback, in a design review with the whiteboard argument, is the skill this question is really probing.

## The wrong instinct, stated precisely The code has a `while` inside a `for`, and the reflex analysis multiplies: `n` outer iterations times up-to-`n` inner iterations equals O(n²). The multiplication is where the reasoning breaks. Multiplying assumes the inner loop can do its worst *on every* outer iteration independently. Here it cannot, because the inner loop consumes a resource that never replenishes: forward positions of the left pointer. ## The aggregate (amortized) argument Count pointer movements rather than loop iterations: - The right pointer starts at the first element and advances once per outer iteration. Over the whole run: at most `n` moves. - The left pointer starts at position 0, advances by one on each inner-loop iteration, and **never moves backward**. It cannot pass the right pointer by more than one. Over the whole run: at most `n` moves. Every unit of work in the scan is attached to one of these moves — an admission on a right-move, an eviction on a left-move. So the total number of admissions plus evictions is at most `2n`, regardless of how the evictions cluster. Equivalently: **each element enters the window at most once and leaves it at most once.** An element that has been evicted is never seen by the inner loop again. Concretely, in a packet-trace scan for the longest burst with at most `k` distinct sources, the trace can be adversarial — long quiet stretches, then a new source that forces the window to collapse almost entirely in one iteration. That one iteration is expensive. But it *spends* left-pointer moves that the rest of the scan now cannot spend: the total across all bursts still cannot exceed `n` evictions. ## Amortized is not average Two distinctions interviewers listen for: 1. **Amortized vs per-operation.** The O(n) total does *not* mean each outer iteration is O(1). A single iteration can evict a large fraction of the window. The guarantee is about the worst-case **sum** over the whole sequence of operations. 2. **Amortized vs average-case.** Average-case analysis assumes a probability distribution over inputs and can fail on unlucky ones. The `2n` bound here holds for **every** input, including adversarial ones — no randomness, no distributional assumption. Calling this "average" is the classic misuse of the word. ## What the bound actually covers The pointer argument bounds the *number* of admissions and evictions at `2n`; it says nothing about their *cost*. Total time is O(n × cost-per-update): | Per-move bookkeeping cost | Overall scan | |---|---| | O(1) worst-case (running sum, counter) | O(n) worst-case | | O(1) expected (typical hash-based counts) | O(n) expected | | O(log n) (ordered structure per move) | O(n log n) | Stating the bound as "O(n) expected" when hash-based state is involved, rather than a flat "O(n)", is the kind of precision that distinguishes a middle-level answer. ## What would genuinely make it quadratic The linearity is a property of the discipline, not of the pattern's name, and two implementation mistakes break it: - **Recomputing from scratch.** If, instead of updating state incrementally on each move, the code rescans the whole window to re-derive its statistics after every edge move, each of the O(n) moves costs O(n), and the scan is O(n²). - **Backtracking the left pointer.** Any variant that moves `left` backward — for instance, restarting the window at an earlier position after a violation — destroys the "each element leaves at most once" accounting, and the 2n cap with it. Both show up in real code more often than the textbook version does, which is why the argument is worth being able to reproduce on demand rather than merely quote.

  • Does the argument still give O(n) if updating the window state on each move is not O(1)?
    No — the pointer argument bounds the number of updates at about 2n, not their cost. Total time is O(n × cost-per-update): with expected-O(1) bookkeeping the scan is O(n) expected; with an O(log n) ordered structure per move it becomes O(n log n). Quoting the pointer bound alone as the running time silently assumes cheap updates.
  • Is any single outer iteration guaranteed to be fast?
    No. One iteration can evict a large fraction of the window in a single burst. The guarantee is aggregate: a burst spends left-pointer moves that can never be spent again, so all bursts together cost at most n evictions. That is precisely what amortized means here — a worst-case bound on the total over the sequence, not on any single step.
  • What implementation mistake would genuinely make the scan quadratic?
    Anything that redoes work: recomputing the window's statistics from scratch after each edge move instead of updating incrementally, or moving the left pointer backward — for example restarting the window after a violation. Both destroy the 'each element enters once and leaves at most once' accounting that the 2n bound rests on.

saying these in an interview costs you the question

  • A while-loop inside a for-loop always means quadratic time
  • Amortized O(1) means every individual iteration is O(1)
  • Amortized means averaged over random inputs
  • The linear bound needs the shrinks to be evenly spread out

context