A senior engineer on my team once shipped a function that scanned a stream of click events for the longest run with no more than three distinct user-agents. They wrote it as a sliding window because the problem said "longest run with at most K". The window expanded on the right and shrunk on the left when the distinct count exceeded three. Tests passed. Production fell over.
The bug was that they recomputed the distinct-count from scratch on every shrink step, scanning the entire current window. The shape looked like sliding window. The work per step was O(window_size), not O(1). The whole thing was O(n^2) in disguise. They knew two pointers cold, recognised "two indices, both moving right", and reached for the technique. The technique was the right shape. The state reuse was missing.
That story is the argument I want to make. Sliding window is not "same-direction two pointers with extra steps". The defining feature is that the answer for the window [i, j+1] can be derived in O(1) from the answer for [i, j] plus a constant-cost adjustment. When that reuse is impossible, you do not have a sliding-window problem; you have a nested loop with prettier variable names.
The reuse property is the whole technique
The window is just two indices, left and right, both walking the array left-to-right. What makes it sliding-window rather than two-pointers is the running state attached to the window: a sum, a count, a frequency map, a max, whatever the problem needs. Each time right advances, the state absorbs one new element in O(1). Each time left advances, the state evicts one old element in O(1). The window never recomputes from scratch.
Total runtime is O(n) because each index crosses the array at most once, and each step does constant work. Drop the constant-time eviction and the whole bound collapses to O(n^2).
The mental check I run before writing any code: "if the window currently spans [i, j] and I move right to j+1, can I update the running state in O(1)? If I move left to i+1, can I evict in O(1)?" If the answer to either is no, I am not actually doing sliding window and I should stop pretending.
Fixed-size windows: when the size is in the prompt
The easy variant. The problem hands you the window size k and asks something about every contiguous k-element range. Maximum sum of k consecutive elements, average, count of even numbers, whatever.
The template is two phases: prime the window, then slide it.
The whole technique lives in that one line: window_sum += arr[i] - arr[i - k]. One add, one subtract, constant time, no rescan. If the running state is more complex than a sum (a frequency map, say), the eviction step decrements the outgoing element's count and deletes the key when it hits zero. Still O(1) amortised.
Variable-size windows: shrink when the predicate breaks
This is where most of the interview problems live, and where most of the bugs do too. The window is no longer fixed-width; it grows on the right and shrinks on the left based on a predicate.
The template I use:
Three things make this work. The outer for advances right exactly n times. The inner while advances left at most n times across the whole function (not per outer iteration). The predicate check and both updates are O(1). Total: O(n).
The bug from the opening anecdote was making the inner step O(window_size) by rescanning. The discipline is: the only allowed work inside the shrink loop is O(1) state maintenance plus the left += 1. If you find yourself iterating over the window, the technique is wrong for the problem, or the state shape is wrong for the technique.
Four problems and the shape they share
These four come up often enough that I keep the templates in muscle memory.
| Problem | Window kind | Running state | Predicate / answer |
|---|---|---|---|
Maximum sum of k consecutive elements | Fixed | running sum | track max(best, sum) after each slide |
Longest substring with at most k distinct chars | Variable | char-frequency map + distinct count | shrink while distinct > k |
Smallest subarray with sum >= target | Variable | running sum | shrink while sum >= target, track min(best, length) |
| Longest substring without repeating chars | Variable | char-last-seen map (or set) | when seeing a repeat inside window, jump left past the previous occurrence |
The shape underneath all four is identical: a window of contiguous indices, a constant-time absorb on the right, a constant-time evict on the left, and a predicate or extremum tracked across the slide. Switching from one problem to another is a matter of swapping the state and the predicate; the loop scaffold does not change.
The traps that make windows lie
Three pitfalls eat junior engineers (and, see opening, occasional senior ones).
First, non-amortised eviction. Recomputing distinct count, max, or any reduction across the whole window per shrink step. Looks fine on small inputs, blows up at scale. If your state cannot be updated in O(1) per step, sliding window is the wrong technique. Use a different structure (a balanced BST or a deque for sliding-window maximum, for example) or accept that the problem is not linear-time.
Second, off-by-one on window length. The length of the window [left, right] inclusive is right - left + 1, not right - left. Half the bugs in sliding-window code I review come from this single mistake. Write it down at the top of the function and refer to it.
Third, wrong predicate position. With variable windows, the predicate check goes inside a while, not an if, because a single right step can violate the predicate by more than one unit (think: a new character that pushes distinct-count well past k because the current window is already full). if would shrink only one slot; while shrinks until the predicate holds again.
When the window is the wrong shape
Sliding window earns its place on a small set of problems. It is the wrong tool more often than the social-media coverage suggests. If the answer requires looking at non-contiguous indices, stop, this is not your technique, you want subset-sum or DP. If the running state cannot be updated in constant time per step (sliding-window median, say, where every shift requires a sorted insertion), the linear-time bound is a fiction; either reach for a deque-based or two-heap variant or accept a higher complexity. If the problem asks about all subarrays rather than the optimum over them, no window will help, the answer space is O(n^2) by definition. The recognition signal I rely on is the reuse property from the first section. When I cannot describe, in one sentence, how absorbing one element and evicting one element keeps the answer correct, the problem is not sliding window. Knowing where the technique fails is what stops it from being the hammer that paints every problem the wrong colour.
