Many array and string problems ask the same question in different costumes: among all contiguous sub-segments of length / weight / property X, which is best? The brute-force answer is to enumerate every interval and inspect it — an instant or blow-up that fails the moment hits a few thousand. The sliding window technique replaces that quadratic with a single linear pass by exploiting one observation: as the right endpoint moves forward, the left endpoint never has to move backward. Two monotone pointers, total work, and a state object that updates incrementally as the window slides.
Sliding window is the workhorse behind moving-average filters in digital signal processing, rate limiters in distributed systems, rolling RMS in audio, k-mer scanning in genomics, time-windowed analytics on infinite streams, and a sizeable fraction of the array and string questions you will be asked in interviews. By the end of this section you will recognize when the technique applies, when it does not, and you will have written it from the universal template six times in two languages.
The Core Idea: A Window That Walks
Define a window as a contiguous range with (or the empty window when). A summary of the window — its sum, its character histogram, its set of distinct elements, its maximum — is what we care about. The technique rests on three operations:
- Add: extend by one on the right,. Cost is typically .
- Remove: shrink by one on the left,. Cost is typically .
- Query: read the current answer from in .
If your problem can be modeled this way, sliding window applies. Concrete summaries:
| Problem | Summary S | Add A[r] | Remove A[l] | Query |
|---|---|---|---|---|
| Max sum, fixed k | running sum | += A[r] | −= A[l] | S |
| Longest no-repeat | hashmap last_seen | last_seen[c] = r | (implicit via l-jump) | r − l + 1 |
| Min window substring | Counter need + missing | need[c]−−; if was needed missing−− | need[c]++; if now needed missing++ | missing == 0? |
| Sum at most K (positive) | running sum | += A[r] | −= A[l] | S ≤ K? |
| Sliding max | monotonic deque | evict tails ≤ A[r]; push r | expire head if past l | A[dq[0]] |
| Permutation in string | freq diff vector + match count | diff at A[r]−− | diff at A[l]++ | matches == |alphabet|? |
Mental model. Think of the window as a centipede crawling along the array. Its head probes forward; its tail follows. Neither head nor tail ever reverses. The summary is what the centipede remembers about the food it has eaten so far.
Why It Is O(n): The Amortized Argument
At first glance the variable-size window looks suspect: an iteration of the outer loop can run an arbitrarily long inner loop that pushes forward many steps. Could the worst case be ?
No — and the proof is the textbook example of aggregate amortized analysis. Charge the algorithm for each pointer move:
- The right pointer advances exactly once per outer loop iteration. Total moves of: at most .
- The left pointer only ever increases (it never decreases). It is bounded above by. Total moves of across the whole algorithm: at most.
- Per-move cost is for sums, hashmap updates, and deque operations.
Total: . The fact that sometimes the inner loop fires hard and sometimes it does not is irrelevant to the aggregate bound — we paid for each pointer move at most once.
Why this is amortized, not worst-case-per-step
Fixed-Size Window: Moving Sums and Averages
The simplest sliding window has a fixed length. Compute the sum (or any associative aggregate) of , then update in as the window steps forward:
Total work: one prime of size plus constant-time updates —. The naive nested-loop version is ; for on a 10M-sample audio buffer that is the difference between 10 milliseconds and 10 seconds.
1def max_sum_window(arr, k):
2 """Maximum sum of any contiguous length-k subarray. O(n) time."""
3 if k <= 0 or k > len(arr):
4 return None
5 s = sum(arr[:k]) # prime: O(k)
6 best = s
7 for r in range(k, len(arr)): # slide: O(n - k)
8 s += arr[r] - arr[r - k] # one add, one subtract
9 best = max(best, s)
10 return bestVariable-Size Window: Grow Then Shrink
Many problems do not pre-fix the window's length. Instead they ask for the longest (or shortest) window satisfying some predicate. The pattern is always:
- Grow the window on the right by adding to the summary.
- While the predicate is violated (or, for "at-most" style problems, would be violated), shrink from the left by removing .
- Once the window is valid, record the candidate answer.
For this pattern to work the predicate must be monotone in the window: if a window is invalid, then every superset with the same is also invalid (so we have to move ); equivalently, removing elements cannot turn a valid window into an invalid one (so we never need to rewind). When this monotonicity fails, sliding window does not apply — see the decision section.
Longest Substring Without Repeating Characters
Given a string , return the length of its longest substring that contains no repeated characters. A naive scan of every pair is tries times an validity check — . With sliding window plus a last-seen hashmap, :
Trace on :
| r | c | action | l | window | best |
|---|---|---|---|---|---|
| 0 | a | new | 0 | a | 1 |
| 1 | b | new | 0 | ab | 2 |
| 2 | c | new | 0 | abc | 3 |
| 3 | a | dup at 0 → l = 1 | 1 | bca | 3 |
| 4 | b | dup at 1 → l = 2 | 2 | cab | 3 |
| 5 | c | dup at 2 → l = 3 | 3 | abc | 3 |
| 6 | b | dup at 4 → l = 5 | 5 | cb | 3 |
| 7 | b | dup at 6 → l = 7 | 7 | b | 3 |
Minimum Window Substring
Given strings and , return the shortest contiguous substring of that contains every character of with full multiplicity. The classic FAANG question; a beautiful illustration of the grow-then-shrink pattern with two-tier bookkeeping (a per-character debt vector plus an aggregate "missing" counter).
Trace on ,: the algorithm expands until index 5 (covering one, ,), shrinks to the candidate (length 6), keeps scanning, finds another valid window at index 10 (, also length 6 after shrinking), and finally at picks up a fresh and shrinks all the way to (length 4). That is the winner.
The two-tier bookkeeping trick. Per-character debts plus an integer let us check validity in . Without we would need to scan the debt dictionary every step — correct, but instead of. For 1M-character DNA sequences with the alphabet the difference is small; for full Unicode it is enormous.
Longest Subarray With Sum at Most K
Given an array of positive integers and an integer, return the length of the longest contiguous subarray whose sum is at most. Grow on the right; while the running sum exceeds , shrink on the left:
1def longest_sum_at_most(arr, K):
2 """Longest contiguous window with sum <= K. arr must be NON-NEGATIVE."""
3 s, l, best = 0, 0, 0
4 for r in range(len(arr)):
5 s += arr[r] # add right
6 while s > K and l <= r: # shrink while invalid
7 s -= arr[l]
8 l += 1
9 best = max(best, r - l + 1) # record
10 return bestWhy positivity matters
Sliding Window Maximum (Monotonic Deque)
Given and a window size, output an array with for. Naive: per-window scan of elements,. A heap solution shaves it to but requires lazy deletion. A monotonic deque nails it at.
The deque stores indices whose values are strictly decreasing. The head is therefore always the argmax of the current window. Two invariants maintain this:
- When a new index arrives, pop tail indices whose values are : they are dominated by a younger, larger element and can never be a future maximum.
- When the window has slid past the head index (head), evict it.
Trace on with :
| r | A[r] | deque (indices) | deque (values) | emit |
|---|---|---|---|---|
| 0 | 1 | [0] | [1] | — |
| 1 | 3 | [1] | [3] | — |
| 2 | -1 | [1, 2] | [3, -1] | 3 |
| 3 | -3 | [1, 2, 3] | [3, -1, -3] | 3 |
| 4 | 5 | [4] | [5] | 5 |
| 5 | 3 | [4, 5] | [5, 3] | 5 |
| 6 | 6 | [6] | [6] | 6 |
| 7 | 7 | [7] | [7] | 7 |
Output: . Each index is pushed once and popped at most once across the whole run — the total work for the inner while loops is bounded by, so the algorithm is in aggregate.
nums[r] by -nums[r] and you get sliding-window minimum.Permutation in a String
Does contain any permutation of as a contiguous substring? Equivalent to: is there an such that the character frequency of equals the frequency of ? Fixed-size window of length ; maintain a per-character difference vector. The window is a match exactly when. To avoid scanning every step, also maintain a counter = number of characters for which . Each window step changes in two slots, so updates in. The answer is yes iff at any step.
1def has_permutation(s1, s2):
2 if len(s1) > len(s2):
3 return False
4 need = [0] * 26
5 have = [0] * 26
6 for c in s1:
7 need[ord(c) - ord('a')] += 1
8 matches = sum(1 for i in range(26) if need[i] == have[i])
9 for r, c in enumerate(s2):
10 i = ord(c) - ord('a')
11 have[i] += 1
12 if have[i] == need[i]:
13 matches += 1
14 elif have[i] - 1 == need[i]: # we just unmatched
15 matches -= 1
16 if r >= len(s1): # window now too big — shrink left
17 j = ord(s2[r - len(s1)]) - ord('a')
18 have[j] -= 1
19 if have[j] == need[j]:
20 matches += 1
21 elif have[j] + 1 == need[j]:
22 matches -= 1
23 if matches == 26:
24 return True
25 return FalseThe Universal Sliding-Window Template
Every sliding-window problem in this section is an instance of the same loop. Memorize this skeleton; the only thing that changes between problems is the choice of summary and the predicate.
1def sliding_window_template(A):
2 l = 0
3 S = init_state() # e.g. running sum, Counter, deque, ...
4 best = init_best()
5 for r in range(len(A)):
6 S = add(S, A[r]) # extend window to include A[r]
7 while not valid(S): # tighten until invariant holds
8 S = remove(S, A[l])
9 l += 1
10 best = update(best, l, r, S)
11 return best- add / remove must be O(1) amortized for the whole template to be . Hashmap counts, running sums, and monotonic deques satisfy this; sorted-set insertion () bumps the total to, which is still fine.
- valid must be monotone: if removing elements from a valid window keeps it valid (or vacuously so), we are safe to shrink without rewinding.
- update is O(1): read off the current best from and / or.
- For at-most problems (longest sub-window with sum ≤ K, with at most k distinct, etc.) shrink while and record after the inner loop. For at-least problems (shortest sub-window covering all of t, etc.) shrink while and record inside the inner loop.
Interactive: The Sliding Window Lab
Pick a problem, edit the inputs, and step the window forward manually or hit Run for auto-play. Green cells are the current window; the blue l arrow and red r arrow mark the boundaries. Watch the "Internal state" panel update as changes — you can literally see the algorithm's memory.
The Sliding Window Lab
Green cells are inside the current window. Blue l and red r mark the window boundaries (both inclusive). Step forward to watch each pointer advance — notice they only ever move right, never left. That is what gives sliding window its linear-time guarantee.
s = "XYXYZAB" and t = "XYZ". Watch how the window grows to XYXYZ, becomes valid, and the inner shrink loop fires twice (dropping the leading X and Y) to land on XYZ. The two phases — expand right, then collapse left — are visible in real time.Quick Check
On the array A = [1, 3, -1, -3, 5, 3, 6, 7] with k = 3, the sliding window MAXIMUM is...
Quick Check
Which of these problems can be solved by a vanilla sliding window in O(n)?
Real-World Applications
Sliding windows are everywhere outside of interview questions; the technique is the algorithmic fabric beneath whole engineering domains:
| Domain | Window content | What is computed |
|---|---|---|
| Audio DSP | k consecutive samples | Moving average for a low-pass filter; rolling RMS for a VU meter; rolling autocorrelation for pitch tracking. |
| Computer vision | k×k pixel patch | Convolution kernel: every output pixel is a weighted sum of a sliding patch — the workhorse beneath every CNN. |
| Network monitoring | Timestamps in the last T seconds | Token-bucket / leaky-bucket rate limiters; SYN-flood detection; per-source request quotas. |
| Stock trading | Last n closing prices | Simple moving averages, Bollinger Bands (mean ± k·σ over the window), MACD crossovers. |
| Genomics | All length-k subwords (k-mers) | GC content scanning, motif discovery, minimizer-based sequence alignment in BWA / minimap2. |
| Search engines | Rolling hash of a 5-shingle of words | Near-duplicate document detection; Rabin–Karp substring search. |
| Operating systems | Last N system calls per process | Anomaly detection, sequence-of-calls intrusion detection. |
| Real-time analytics | Events in last 5 min | Tumbling vs sliding vs hopping windows in Kafka Streams, Flink, ksqlDB. |
| Trading order books | Trades in last 30s | Time-windowed VWAP, microstructure metrics, rolling order-book imbalance. |
| SRE / observability | Last 1000 events | p50/p95/p99 latency trackers using approximate sketches over a rolling window. |
Streams change the rules
Pitfalls and Common Bugs
- Inclusive vs exclusive bounds. Decide once, at the top: is the window (both inclusive, length ) or (right-exclusive, length )? Mixing the two within one function is a classic off-by-one factory.
- Forgetting to tighten. If your while-loop is
ifinstead ofwhile, the window can contract by only one step per iteration and you may miss valid configurations — or worse, leave the invariant violated and silently emit a wrong answer. - Hashmap leftovers. When the window slides off a character, decrement (or delete) its entry in the summary. Stale counts produce ghosts — the dictionary remembers characters that are no longer in the window and the predicate fires falsely.
- Negative or zero values in "sum at most K" or "product at most K" problems break monotonicity. Check the input domain explicitly. AC-coupled sensor data, log-returns on stock prices, and signed deltas are common real-world instances where this bites.
- Empty-window edge cases. If can fall behind (it should not, in the canonical template), your add / remove logic must handle the empty window cleanly. Most bugs here come from initializing before the first iteration and then double-counting on the first add.
- Counting answers vs lengths. "Number of subarrays with sum at most K" is the cumulative sum of over all — not just the maximum. Easy to confuse with "longest subarray with sum at most K."
When Sliding Window Does Not Apply
Sliding window's linear-time guarantee depends on the non-rewinding property of . When validity is not monotone, that property fails. The replacement-of-choice depends on which monotonicity is violated:
| Problem | Monotonicity issue | Replacement |
|---|---|---|
| Subarray with sum exactly K (signed) | Adding / removing can jump validity in either direction | Prefix sum + hashmap of seen prefix sums (Ch 9) |
| Subarray with sum at most K (signed) | Removing a negative can break validity | Prefix sum + monotonic deque or balanced BST |
| k-th smallest in window (general) | Smallest is not preserved by add / remove without re-sorting | Order-statistic tree, two heaps, or wavelet tree |
| Median in window | Same as above | Two heaps with lazy deletion, or order-statistic tree |
| Mode (most frequent) in window | No O(1) update for the argmax of a multiset under add / remove | Wavelet tree or sqrt-decomposition (advanced) |
| All windows with sum exactly K (positives only) | Monotone — sliding window does work here | Vanilla sliding window: shrink while sum > K, count when sum == K |
- Identify the summary and predicate .
- Check: if holds, does hold? (Monotone shrinkage.)
- Check: are add / remove both amortized?
- If both yes ⇒ sliding window. Plug into the template.
- If no ⇒ reach for prefix sums + hashmap, monotonic deques, balanced BSTs, or two-heap medians. Each pattern has a chapter ahead.
Sliding window is the first "design pattern" of array algorithms: a small, reusable mental machine that turns brute force into clean code. Everything else in this chapter — two pointers, prefix sums, dynamic arrays — will keep cross-referencing it. Master the template now, and three chapters from now monotonic stacks and deques (Ch 7) will feel like minor variations on this same theme.