Sliding Window (Variable Size)
Grow a window from the right while a condition holds and shrink it from the left when it breaks, finding the longest or shortest valid contiguous subarray in O(n).
Overview
A variable-size sliding window finds the longest or shortest contiguous subarray satisfying a constraint — sum ≥ s, at most k zeros flipped, no repeated characters, at most k distinct values. Two indices l and r delimit the window; r always moves right to admit a new element, and l moves right only as far as needed to restore the constraint. Both pointers move at most n times, so the total is O(n) even though the inner shrink loop looks nested.
The technique applies when the constraint is monotone with respect to inclusion: if a window is valid, every sub-window is valid (for "longest" problems), or if a window is invalid, every super-window is invalid. Sum of non-negative numbers, count of distinct characters, and count of a specific value are all monotone. Sums with negative numbers are not, and then the window pattern fails — that case belongs to Prefix Sum with a Hash Map or a Monotonic Queue.
Intuition
A mental model before the formal terms.
A caterpillar crawling along a branch: it stretches its head forward as long as the branch can hold it, and when the stretch becomes too much it pulls its tail up. The head never goes backward and the tail never goes backward, so the caterpillar traverses the branch in one trip while trying every stretch length it can support at each position.
For "longest window with at most k distinct letters", imagine a bag that can hold k kinds of letters. Keep tossing in the next letter; the moment there are k + 1 kinds, throw out letters from the oldest end until one kind disappears. The bag was as full as it could legally be at every step, so the biggest bag you ever saw is the answer.
How it works
- Initialize
l = 0and an empty aggregate (sum, count, frequency map) describing the windowa[l..r]. - For each
rfrom0ton − 1: adda[r]to the aggregate — the window grows by one on the right. - While the window violates the constraint: remove
a[l]from the aggregate andl++. The window shrinks from the left until valid again. - For longest-type problems, the window is now valid: record
r − l + 1if it beats the best. - For shortest-type problems, flip the roles: shrink *while the window is still valid* and record the length just before it stops being valid (or after each successful shrink).
- Return the best length (or the window bounds, or the count of valid windows if the problem asks for a count — see interview patterns).
Why it works
Invariant (longest variant): after processing r, the window [l, r] is the *longest valid window ending at r* — i.e. l is the smallest index such that a[l..r] is valid. This holds because monotonicity means validity of a[l..r] implies validity of a[l'..r] for all l' > l, so the valid left endpoints for a fixed r form a suffix [l*, r], and the shrink loop stops exactly at l*.
Why `l` never needs to move backward: when r advances to r + 1, any window a[l'..r+1] with l' < l contains a[l'..r], which was already invalid (that is why l moved past it). By monotonicity the larger window is invalid too. So every left endpoint discarded for r is also useless for r + 1, and no valid answer is ever skipped.
Because the best window ending at each r is examined, the global optimum — which ends at *some* r — is found. l and r each advance at most n times, so the total number of aggregate updates is at most 2n: O(n) time.
If the constraint is not monotone (e.g. sum ≥ s with negative numbers), the valid left endpoints for a fixed r are not a contiguous suffix and the shrink loop can stop too early; the invariant breaks and the answer can be wrong.
Recognition
How to tell a problem wants this.
- "Longest" or "shortest"/"minimum length" paired with "contiguous", "subarray", or "substring".
- A capacity-style constraint: "at most `k`" (distinct characters, zeros, replacements, types of fruit), "sum ≥
s", "no repeated characters", "with at mostkoperations". - All values are non-negative (for sum constraints) — a strong hint that the sum is monotone and a window will work; negative values are the hint that it will not.
- "Count the number of subarrays with…" plus a monotone constraint — the same window, but add
r − l + 1per step instead of taking a max (for "at most"), and use "at mostk" minus "at mostk − 1" for "exactlyk". - Constraints
n ≤ 10^5or10^6with a brute force that enumeratesO(n²)subarrays.
Interactive visualization
Play, step, change the input. ← → and space work too.
1l = 0, sum = 0, best = ∞2for r in 0 .. n-1:3 sum += a[r]4 while sum >= target:5 best = min(best, r - l + 1)6 sum -= a[l]; l += 17return best (or 0 if never reached)Pseudocode
1l = 0, best = 0, state = empty2for r in 0..n-1:3 add a[r] to state4 while state violates constraint:5 remove a[l] from state6 l = l + 17 best = max(best, r - l + 1) # window [l, r] is the longest valid one ending at r8return bestImplementations
1# Max Consecutive Ones III: longest subarray of 1s after flipping at most k zeros2def longest_ones(a: list[int], k: int) -> int:31 · Window [l, r] with a count of zeros inside it4 l = 05 zeros = 06 best = 072 · Expand the window by one on the right8 for r, x in enumerate(a):9 if x == 0:10 zeros += 1113 · Shrink from the left while the constraint is violated12 while zeros > k:13 if a[l] == 0:14 zeros -= 115 l += 1164 · Window is valid: record its length17 best = max(best, r - l + 1)185 · Longest valid window seen19 return bestenumerate(a)yields(r, x)so the entering element is read once without a second index expression.zerosis the whole window state; aCounterwould be overkill for a single tracked value.- The
while zeros > kloop shrinks from the left, checkinga[l]before advancingl. best = max(best, r - l + 1)records the valid window length.- The function returns an
int.
Slicing a[l:r + 1] to inspect the window would copy — O(window) per step; the code never slices.
- For "at most k distinct" use
collections.Counterordefaultdict(int)for the window, and delete keys when their count hits 0 solen(window)stays the distinct count. enumerateavoidsrange(len(a))plus indexing.- The inverted shrink (
while total >= target) gives the shortest-window template.
- Using
a[l:r + 1].count(0)inside the loop — O(n^2). - Using
ifinstead ofwhileto shrink. - Forgetting to update
zeroswhena[l]leaves.
- When the window state is a frequency table (the "at most k distinct" family): Python uses
Counter/defaultdict, C++std::unordered_mapor a fixedstd::array<int,26>, JS/TSMap(object keys would stringify numbers). - TypeScript can express the binary precondition as
(0 | 1)[]; C++, JS and Python accept any ints and rely on the data. - Inspecting the window by slicing (
a[l:r+1],a.slice(l, r+1)) copies in Python and JS/TS; C++ iterators or index arithmetic do not — the algorithm should never need the slice.
Complexity
Each pointer advances at most n times. Space O(k) or O(Σ) when the window state is a frequency map.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Longest/shortest contiguous subarray or substring under a monotone constraint.
- Counting subarrays satisfying an "at most
k" property (sum with non-negative values, distinct elements, occurrences of a value). - Constraints expressed as budgets: at most
kflips, replacements, or distinct kinds. - Any time you catch yourself writing
for l: for r:over a contiguous range with a monotone check.
- The constraint is not monotone: subarray sum equals
kwith negative numbers, "exactlykdistinct" directly (use the at-most-kminus at-most-k−1trick), or conditions involving max/min differences that a shrink can *fix* from either side. - The problem is about subsequences, not contiguous ranges.
- Two-dimensional windows over a matrix — combine 2D Prefix Sum with a window over one dimension instead.
- Very small
nwhere theO(n²)enumeration is clearer and fast enough — but say so explicitly.
Alternatives
Common mistakes
- Using
ifinstead ofwhileto shrink — one removal may not restore validity. - Updating
bestbefore shrinking in a longest-type problem (window may be invalid), or after shrinking in a shortest-type problem (window is now invalid, length is one too short). - Applying the window to a sum constraint with negative numbers and getting wrong answers on tests with mixed signs.
- Forgetting to remove
a[l]from the aggregate before incrementingl. - For counting "exactly
k", trying to count directly inside the loop instead of computingatMost(k) − atMost(k−1).
Interview patterns
- Longest Substring Without Repeating Characters (window with a last-seen map or a set).
- Max Consecutive Ones III / Longest Repeating Character Replacement (budget of
kchanges; the latter useswindow − maxFreq ≤ k). - Minimum Size Subarray Sum (shortest, positive numbers).
- Fruit Into Baskets / Longest Substring with At Most K Distinct Characters.
- Subarrays with K Different Integers via
atMost(k) − atMost(k−1); Count Number of Nice Subarrays with the same trick on odd counts. - Binary Subarrays With Sum (0/1 array): at-most trick again, or Prefix Sum with a hash map if values can be negative.
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- When space complexity mattersIntermediate
- Recognizing a sliding-window problemIntermediate
- Minimum Size Subarray SumIntermediate
- Two SumBeginner
- Longest Substring Without Repeating CharactersIntermediate