Sliding Window
Keep a window over a contiguous range and update it incrementally as it slides, instead of recomputing every subarray.
- O(n)Time: each element enters and leaves once
- O(a)Space for counts over an alphabet of size a
- 2n movesAt most; neither pointer ever moves back
Show what to say
“When I need the longest, shortest or number of contiguous ranges under a rule like at most k distinct, I keep a window with a running summary. I grow right, and shrink left while the rule is broken, so each element enters and leaves once: O(n) instead of O(n · k).”
Trade-offs
- Sliding window vs prefix sums: a window is O(1) space but needs non-negative values; prefix sums plus a map handle negatives in O(n) space.
- Monotonic deque vs heap for window maximum: the deque is O(n); a max-heap that pops stale tops is O(n log n) but easier to explain.
- Exactly k vs at most k: exactly(k) = atMost(k) - atMost(k - 1), two O(n) passes, because only the at-most rule is monotonic.
Bring it up before they ask
Shrink with while, not if, and watch the off-by-ones: the length is right - left + 1 and the first full window of size k ends at index k - 1.
A sliding window is a frame over a row that slides along instead of starting over. Each element enters once and leaves once, so O(n · k) becomes O(n).
When to reach for it
Tap the signals you can see in the problem.
Remembercontiguousat mostnofixed size
- Fixed size
How the edges movetogether: one in, one out
maximum average subarray - Longest valid
How the edges movegrow
longest substring without repeatsright; while invalid, moveleft - Shortest valid
How the edges movegrow
minimum window substringright; while valid, moveleft - Monotonic deque
How the edges movea deque keeps max or min candidates
sliding window maximum
The core idea: update, don't recompute
Picture a frame over a row of boxes.
Read more · 106 words
Slide it one step: one box comes in, one goes out. Update the running total with those two changes instead of re-adding everything.
A window that grows and shrinks needs a rule that only gets harder (or only easier) as the window grows.
- At most rules (k distinct, no repeats): once a window breaks the rule, every longer one from the same
leftdoes too. Soleftmoves on for good. - At least rules (sum at least target): once valid, longer windows can't beat it. So
leftis done.
Neither pointer moves back: at most 2n moves, so O(n) despite the while inside the for.
Fixed window: best sum of k consecutive values
- k
- 3
- sum
- 2
Build the first window of size 3 from scratch, the only time we add up k values. Add 2: sum = 2.
function maxWindowSum(nums, k) {
let sum = 0;
for (let i = 0; i < k; i++) sum += nums[i];
let best = sum;
for (let right = k; right < nums.length; right++) {
sum += nums[right] - nums[right - k];
best = Math.max(best, sum);
}
return best;
}All steps as text
- Build the first window of size 3 from scratch, the only time we add up k values. Add 2: sum = 2.
- Add 1: sum = 3.
- Add 5: sum = 8.
- The first full window [2, 1, 5] sums to 8. It's the best so far.
- Slide right by one: 1 enters and 2 leaves, so sum = 8 + 1 - 2 = 7. Two operations instead of re-adding 3 values.
- 7 doesn't beat 8, so best stays.
- Slide right by one: 3 enters and 1 leaves, so sum = 7 + 3 - 1 = 9. Two operations instead of re-adding 3 values.
- 9 beats the previous best: best = 9.
- Slide right by one: 2 enters and 5 leaves, so sum = 9 + 2 - 5 = 6. Two operations instead of re-adding 3 values.
- 6 doesn't beat 9, so best stays.
- Every window of size 3 was visited once, each in O(1). The best window is [5, 1, 3] with sum 9.
Play with it
Type a word into Type your own letters: the window stretches right and snaps left whenever a letter repeats.
Read more · 66 words
The recipe:
Check that a window fits
Negative numbers fail: use Prefix Sums.
Choose the window's state
The smallest summary that answers "valid?" in O(1): a sum, or a
Mapof counts.Write add(right) and remove(left)
removemust undoadd.Decide when left moves
Longest: while invalid. Shortest: while valid.
Record at the right moment
Longest: after the shrink loop. Shortest: inside it.
Templates
Template 1: fixed-size window
Slide: add nums[right], subtract nums[right - k].
Maximum Average SubarrayEasy is this template divided by k.
// Template 1: fixed-size window. Neighbouring windows share k - 1 elements, so update the summary
// with the element that enters and the one that leaves instead of recomputing it: O(n), not O(n · k).
export function maxWindowSum(nums, k) {
if (k <= 0 || k > nums.length) return null;
let sum = 0;
for (let i = 0; i < k; i++) sum += nums[i]; // the first full window ends at index k - 1
let best = sum;
for (let right = k; right < nums.length; right++) {
sum += nums[right] - nums[right - k]; // nums[right] enters, nums[right - k] leaves
best = Math.max(best, sum);
}
return best;
}Template 2: longest valid window
Grow right every step. If the new element breaks the rule, move left until it holds.
Read more · 30 words
What is left is the longest valid window ending at right; keep the best.
Longest Substring Without Repeating CharactersMedium uses "no character twice". Longest Repeating Character ReplacementMedium uses "length minus top letter count is at most k".
Variable window: longest substring with at most k distinct characters
- k
- 2
- left
- 0
- best
- 0
The window s[left..right] may hold at most 2 distinct characters. The map counts the characters inside it; its size is the number of distinct ones.
function longestAtMostKDistinct(s, k) {
const count = new Map();
let left = 0, best = 0;
for (let right = 0; right < s.length; right++) {
count.set(s[right], (count.get(s[right]) ?? 0) + 1);
while (count.size > k) {
const out = s[left];
if (count.get(out) === 1) count.delete(out);
else count.set(out, count.get(out) - 1);
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}All steps as text
- The window s[left..right] may hold at most 2 distinct characters. The map counts the characters inside it; its size is the number of distinct ones.
- Extend right to 0: 'a' enters. The window now has 1 distinct character.
- Valid window "a" has length 1: a new best.
- Extend right to 1: 'b' enters. The window now has 2 distinct characters.
- Valid window "ab" has length 2: a new best.
- Extend right to 2: 'c' enters. The window now has 3 distinct characters.
- 3 distinct is more than 2: the window is invalid. Any window that still starts at 0 and ends at 2 or later is invalid too, so left must move.
- 'a' leaves, its count drops to 0 and it is deleted. Down to 2 distinct: valid again.
- Valid window "bc" has length 2; best stays 2.
- Extend right to 3: 'b' enters. The window now has 2 distinct characters.
- Valid window "bcb" has length 3: a new best.
- Extend right to 4: 'b' enters. The window now has 2 distinct characters.
- Valid window "bcbb" has length 4: a new best.
- Extend right to 5: 'c' enters. The window now has 2 distinct characters.
- Valid window "bcbbc" has length 5: a new best.
- Extend right to 6: 'a' enters. The window now has 3 distinct characters.
- 3 distinct is more than 2: the window is invalid. Any window that still starts at 1 and ends at 6 or later is invalid too, so left must move.
- 'b' leaves (2 still inside). Still 3 distinct: keep shrinking.
- 'c' leaves (1 still inside). Still 3 distinct: keep shrinking.
- 'b' leaves (1 still inside). Still 3 distinct: keep shrinking.
- 'b' leaves, its count drops to 0 and it is deleted. Down to 2 distinct: valid again.
- Valid window "ca" has length 2; best stays 5.
- right reached the end. Each index entered once and left at most once, so this was O(n). Longest valid window: "bcbbc".
// Template 2: variable window, LONGEST valid. Grow right; while the window breaks the rule, shrink left;
// then the window is the longest valid one ending at right. Works when shrinking a valid window keeps it valid.
// Example rule: at most k distinct characters.
export function longestAtMostKDistinct(s, k) {
const count = new Map(); // character → copies inside s[left..right]
let left = 0;
let best = 0;
for (let right = 0; right < s.length; right++) {
count.set(s[right], (count.get(s[right]) ?? 0) + 1);
while (count.size > k) { // invalid: too many distinct characters
const out = s[left];
if (count.get(out) === 1) count.delete(out); // delete at zero so size stays honest
else count.set(out, count.get(out) - 1);
left++;
}
best = Math.max(best, right - left + 1); // record AFTER shrinking
}
return best;
}Template 3: shortest valid window
Grow until valid. Then squeeze: while valid, record and move left.
Minimum Size Subarray SumMedium is this template.
// Template 3: variable window, SHORTEST valid. Grow right until the window is valid; then, while it stays
// valid, record it and shrink left. Example rule: sum >= target, with positive numbers only.
export function shortestSumAtLeast(nums, target) {
let left = 0;
let sum = 0;
let best = Infinity;
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) { // valid: record BEFORE shrinking
best = Math.min(best, right - left + 1);
sum -= nums[left];
left++;
}
}
return best === Infinity ? 0 : best;
}Template 4: monotonic deque for window max or min
A sum survives an element leaving; a maximum doesn't.
Read more · 31 words
Keep a deque of indices with decreasing values. Pop smaller or equal ones from the back before pushing, and drop the front once it slides out. The front is the maximum.
Monotonic deque: maximum of every window of size k
- k
- 3
- i
- null
The deque holds indices whose values decrease from front to back. Its front is always the current window's maximum.
function windowMax(nums, k) {
const dq = []; let head = 0; const out = [];
for (let i = 0; i < nums.length; i++) {
if (head < dq.length && dq[head] <= i - k) head++;
while (dq.length > head && nums[dq.at(-1)] <= nums[i]) dq.pop();
dq.push(i);
if (i >= k - 1) out.push(nums[dq[head]]);
}
return out;
}All steps as text
- The deque holds indices whose values decrease from front to back. Its front is always the current window's maximum.
- Push index 0 (value 5) at the back. Values in the deque still decrease from front to back.
- Push index 1 (value 3) at the back. Values in the deque still decrease from front to back.
- 4 arrives. 3 is no bigger than 4 and will leave the window before it, so it can never be a maximum again: pop from the back.
- Push index 2 (value 4) at the back. Values in the deque still decrease from front to back.
- The window 0..2 is full. Its maximum is the front of the deque: 5.
- Index 0 (value 5) is no longer inside the window 1..3: drop it from the front.
- Push index 3 (value 1) at the back. Values in the deque still decrease from front to back.
- The window 1..3 is full. Its maximum is the front of the deque: 4.
- 6 arrives. 1 and 4 are no bigger than 6 and will leave the window before it, so they can never be a maximum again: pop from the back.
- Push index 4 (value 6) at the back. Values in the deque still decrease from front to back.
- The window 2..4 is full. Its maximum is the front of the deque: 6.
- Push index 5 (value 2) at the back. Values in the deque still decrease from front to back.
- The window 3..5 is full. Its maximum is the front of the deque: 6.
- 2 arrives. 2 is no bigger than 2 and will leave the window before it, so it can never be a maximum again: pop from the back.
- Push index 6 (value 2) at the back. Values in the deque still decrease from front to back.
- The window 4..6 is full. Its maximum is the front of the deque: 6.
- Each index was pushed once and popped at most once, so the whole scan is O(n), not O(n · k). Maxima: [5, 4, 6, 6, 6].
// Template 4: monotonic deque. Holds indices whose values DECREASE from front to back, so the front is
// always the maximum of the current window. Every index is pushed once and popped once: O(n) overall.
export function windowMax(nums, k) {
const dq = []; // indices; dq[head..] is the live part of the deque
let head = 0; // moving head instead of dq.shift(), which is O(n)
const out = [];
for (let i = 0; i < nums.length; i++) {
if (head < dq.length && dq[head] <= i - k) head++; // front index slid out
while (dq.length > head && nums[dq.at(-1)] <= nums[i]) dq.pop(); // can never be a max again
dq.push(i);
if (i >= k - 1) out.push(nums[dq[head]]); // window [i - k + 1, i] is full
}
return out;
}Try it
// Edit the inputs and press Run. These are the four templates on this page.
console.log(maxWindowSum([2, 1, 5, 1, 3, 2], 3)); // 9: the window [5, 1, 3]
console.log(longestAtMostKDistinct('abcbbca', 2)); // 5: "bcbbc"
console.log(shortestSumAtLeast([3, 1, 4, 1, 5, 9], 12)); // 2: [5, 9]
console.log(windowMax([5, 3, 4, 1, 6, 2, 2], 3)); // [5, 4, 6, 6, 6]Complexity
Read more · 44 words
| Variant | Time | Extra space |
|---|---|---|
| Fixed window | O(n) | O(1) for a sum |
| Longest or shortest valid window | O(n) | O(a) for an alphabet of size a |
| Monotonic deque | O(n) | O(k) |
| Brute force | O(n · k) or O(n²) | O(1) |
JavaScript you'll use
JavaScript refresher: Map and Set Strings and characters Stacks, queues and dequesRead more · 28 words
Always delete a key when its count hits zero, or count.size lies.
const len = right - left + 1; // window length with inclusive ends
count.set(ch, (count.get(ch) ?? 0) + 1); // a character enters
if (count.get(out) === 1) count.delete(out); // one leaves: delete at zero,
else count.set(out, count.get(out) - 1); // so count.size = number of distinct
const counts = new Array(26).fill(0); // lowercase letters only: faster than a Map
counts[s.charCodeAt(i) - 97]++;
let head = 0; // deque front: dq[head++] instead of dq.shift()Interview questions
Why does the window break with negative numbers?
Adding an element can shrink the sum, so neither pointer has a safe move. Use prefix sums plus a hash map.
How do you count subarrays with exactly k distinct values?
exactly(k) = atMost(k) - atMost(k - 1). Two O(n) passes.
Sliding window maximum: why a deque instead of a heap?
A heap that pops stale tops is O(n log n). The deque is O(n).
How would you keep a moving average over a live stream?
Keep the last k values and a running sum: add the new one, subtract the one it overwrites.
Check yourself
Question 1 of 3Score 2 out of 3 or better to mark this page complete.
Practice problems
Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.
- Maximum Average SubarrayFind the highest average of any k consecutive numbers in an array.Easy
- Longest Substring Without Repeating CharactersFind the length of the longest stretch of a string in which no character appears twice.Medium
- Fruit Into BasketsFind the longest run of consecutive trees that contains at most two kinds of fruit.Medium
- Minimum Size Subarray SumFind the length of the shortest run of positive numbers whose sum reaches a target.Medium
- Longest Repeating Character ReplacementChange at most k letters to make the longest possible run of one repeated letter.Medium
- Permutation in StringDecide whether some rearrangement of one string appears as a contiguous piece of another.Medium
- Minimum Window SubstringFind the shortest piece of a string that contains every character of another string, repeats included.Hard
- Sliding Window MaximumReport the maximum of every window of size k as it slides across an array.Hard
In real systems
This pattern is not only for coding rounds. Here is where it does the work in a design interview.