What problem this solves
Fixed-Size Sliding Window handles the case where k is handed to you. Most real interview problems are less generous: they ask for the longest substring satisfying some property, or the shortest subarray meeting some threshold — the window size is not an input, it's the output. Brute force means checking O(n²) start/end pairs (and, if evaluating each pair costs O(n), O(n³) overall). The variable-size sliding window collapses this to O(n) by growing the window greedily on the right and shrinking it on the left only when a correctness invariant is violated, reusing all the incremental state from the previous position instead of re-scanning.
How to recognize it in a problem statement
- "Longest/shortest contiguous subarray/substring such that <condition>" — the word "contiguous" is the tell; if the problem allows non-contiguous selection, you're likely looking at Dynamic Programming or a hash-map counting trick instead (e.g. "subarray sum equals k" with negative numbers is a prefix-sum + hash map problem, not sliding window, because a growing window's sum isn't monotonic when negatives are allowed).
- A monotonic "badness" condition: as the window grows, some property can only get worse (more distinct characters, more replacements needed, sum only increases because all values are non-negative). This monotonicity is what guarantees the left pointer never needs to move backward.
- Two flavors show up almost every time: maximize the window subject to "stays valid" (longest substring with at most K distinct characters), or minimize the window subject to "becomes valid" (shortest subarray whose sum is at least a target).
Mechanics: expand, then shrink-while-invalid
Unlike the fixed-size template, the shrink step here is a while loop, not a conditional — a single character/element entering the window can require the left pointer to jump multiple positions to restore validity.
def variable_window_template(arr, is_valid_check):
left = 0
window_state = init_state() # e.g. a Counter, a running sum, a distinct-count
best = 0 # or float("inf") for "shortest" variants
for right in range(len(arr)):
add(window_state, arr[right]) # 1. Expand
while not is_valid_check(window_state): # 2. Shrink while invalid
remove(window_state, arr[left])
left += 1
best = update(best, right, left) # 3. Record the answer
# (max window for "longest",
# min window for "shortest")
return bestThe two flavors differ only in where you record the answer and what "invalid" means:
- Longest-with-condition (e.g. "at most K distinct characters"): the
whileloop's condition is "window currently violates the constraint" (distinct count > K). You updatebestafter the shrink loop, when the window is guaranteed valid again — you're looking for the longest valid window. - Shortest-with-condition (e.g. "sum >= target"): the
whileloop's condition is "window currently satisfies the constraint" (sum >= target), and you shrink as far as possible while it still holds, updatingbestinside the loop on every valid state, because you want the smallest window that still works, and shrinking further might still be valid.
The at-most-K vs. exactly-K trick
A frequent interview curveball: "find the number of subarrays with exactly K distinct integers." A single sliding window can't track "exactly K" directly, because validity isn't monotonic in a way a simple two-pointer window can maintain (both growing and shrinking can move you into or out of exactly-K). The standard resolution is the subtraction trick:
exactly(K) = atMost(K) - atMost(K - 1)
You write one clean, monotonic helper — atMost(K): count subarrays with at most K distinct values, using the standard shrink-while-invalid window — and call it twice. This works because "at most K" is monotonic (adding elements can only keep distinct-count the same or increase it), so the helper is a straightforward application of the longest-with-condition template, just counting right - left + 1 valid subarrays ending at right instead of tracking a max length. This trick generalizes: any "exactly K" counting problem where "at most K" is easy to compute is a candidate for this subtraction.
Other recurring variations
| Variation | Window state | Shrink condition |
|---|---|---|
| At-most-K distinct | Hash map of value → count, plus len(map) | len(map) > K |
| Sum-based threshold (non-negative values only) | Running sum | sum >= target (shrink to minimize) or sum > target (shrink to stay valid) |
| Character-replacement budget | Frequency array + running max frequency in window | (window length - max frequency) > k |
| Two-basket / at-most-2-types | Hash map of value → count | len(map) > 2 |
| Anagram/permutation coverage | Two frequency maps + a "satisfied count" | Not this shape — this is actually the fixed-size pattern, since the target length is fixed |
Complexity analysis
- Time: O(n) amortized, not O(n²), even though there's a
whileloop nested inside aforloop. The argument:rightadvances exactlyntimes total, andleftalso advances at mostntimes total across the entire run (it only ever moves forward, never resets). So the combined work done by all iterations of the innerwhileloop, summed across the whole outer loop, is bounded byn, notnper outer iteration. This is the amortized analysis you first saw in Big-O & Complexity Analysis, applied concretely: the "nested loop that looks like O(n²)" is actually O(n) because the total number of pointer movements, not the loop nesting, is what bounds the work. - Space: O(1) for a running sum/count; O(min(n, alphabet size)) for a frequency map over a bounded alphabet; O(n) if the window state is a hash map keyed by arbitrary values with no bound on distinct keys.
Pitfalls and interview gotchas
- Assuming non-monotonicity away. The sum-based variant only works because all values are non-negative (or all the same sign) — with negative numbers, growing the window doesn't monotonically increase the sum, so shrinking it doesn't have a reliable stopping rule. If you see negative numbers and a "subarray sum" ask, pivot to prefix sums + hash map (Arrays & Hashing topic), not sliding window.
- Off-by-one when computing the window length. It's
right - left + 1, notright - left— a classic silent-failure bug that under-counts by one for every window. - Updating
bestin the wrong place. For "shortest valid window" problems, you must update the answer inside the shrink loop (every time the window is still valid, before it becomes invalid again), not just once after the loop exits — otherwise you'll only ever record the last valid state you shrunk to, not the best one seen along the way. - Forgetting to undo state on shrink. Every
addneeds a symmetricremove; if your frequency map only increments and never decrements asleftadvances, the window's state silently drifts out of sync with its actual contents. - The "at most K" helper double-counting or under-counting. When counting subarrays (not just tracking a max/min length), the number of new valid subarrays added at each step is
right - left + 1(all subarrays ending atrightstarting anywhere fromlefttoright), not1. This is the detail that makes or breaks the at-most-K vs. exactly-K trick. - Confusing "window is currently invalid" with "window was just invalidated by the last add." Some implementations reset an entire counter and rescan instead of doing an O(1) incremental check — this is correct but silently degrades your complexity back toward O(n · window size).
Fixed-size vs. variable-size at a glance
| Fixed-size | Variable-size | |
|---|---|---|
| Window length | Given (k) | Discovered; the answer itself |
| Shrink trigger | Every step, once window reaches size k | Only when the invariant breaks (0+ times per step) |
| Loop shape | for + if | for + inner while |
| Answer recorded | Once per full window | After shrink loop (longest) or inside it (shortest) |
A worked pattern (not one of your assigned problems)
To see the shrink-while-invalid mechanic without touching your assigned problems, consider a made-up variant: given an array of positive integers and a cap C, find the length of the longest subarray whose product is strictly less than C.
def longest_subarray_product_under_cap(nums, cap):
if cap <= 1:
return 0
left = 0
product = 1
best = 0
for right, val in enumerate(nums):
product *= val
while product >= cap:
product //= nums[left]
left += 1
best = max(best, right - left + 1)
return bestEvery element is multiplied in once (right) and divided out at most once (left), so the whole scan is O(n) despite the nested loop — the same amortized argument as before, just with a product instead of a sum or a frequency map.
Where this connects
This is the more general sibling of Fixed-Size Sliding Window, and both are specializations of the same-direction two-pointer discipline from the Two Pointers topic — the pointers here never cross and never move backward, they just grow and shrink a live window instead of converging from opposite ends. When the "shrink" logic needs to track a running maximum efficiently rather than a sum or frequency count, you're often one step away from needing a monotonic structure, which is exactly where the upcoming Monotonic Stack topic picks up.
Further Resources (Optional)
- GeeksforGeeks — Sliding Window Problems: Identify, Solve and Interview QuestionsArticle15m
- NeetCode — Longest Substring Without Repeating CharactersVideo7m
- NeetCode — Minimum Window Substring (Airbnb Interview Question)Video26m
- LeetCode Discuss — How To Solve ANY Sliding Window Problem (fixed / variable / exactly-K templates)Article15m
- Codeforces — Two Pointers Technique, Explained with Examples (Longest/Shortest Window Templates & Proofs)Article20m
- USACO Guide — Two Pointers (Silver): Sliding Window Derivation and Complexity ProofCourse30m
- Aditya Verma — Sliding Window Algorithm Playlist (Face to Face Interviews)Video4h
- interviewing.io — Sliding Window Interview Questions & Tips for Senior EngineersArticle15m
Practice Problems
Interview relevancy:!!!Critical — must-have!!Important — highly recommended!Good to have~Niche — rarely asked
- Longest Substring Without Repeating CharactersMedium!!!2/525m
- Minimum Size Subarray SumMedium!!2/525m
- Fruit Into BasketsMedium!!!3/525m
- Max Consecutive Ones IIIMedium!!3/525m
- Longest Repeating Character ReplacementMedium!!3/530m
- Minimum Window SubstringHard!!!4/545m
- Subarrays with K Different IntegersHard!!5/545m
Optional Practice (Extra Reps)
For once you've cleared the main set above and want more reps on this pattern. These don't count toward the roadmap's progress stats — solve them purely for your own benefit.
- Frequency of the Most Frequent ElementMedium!3/530m
- Count Number of Nice SubarraysMedium!3/530m
- BooksCodeforces~2/520m
- Contains Duplicate IIIHard!4/535m
- Minimum Window SubsequenceHardPremiumFree replacement~5/540m