Pattern 3 of 24 · Arrays & strings · 30 min

Sliding Window

Keep a window over a contiguous range and update it incrementally as it slides, instead of recomputing every subarray.

Spot it:contiguous subarray or substringlongest / shortest / at most kfixed window of size kJavaScript you'll use: Map and Set Strings and characters Stacks, queues and deques
Say it in 30 seconds
  • 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.

0 of 0 sections ticked
30-second answer

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

Spot it in the problem0 of 3

Tap the signals you can see in the problem.

Remembercontiguousat mostnofixed size

Pick your variant
  • Fixed size

    How the edges movetogether: one in, one out

    maximum average subarray
  • Longest valid

    How the edges movegrow right; while invalid, move left

    longest substring without repeats
  • Shortest valid

    How the edges movegrow right; while valid, move left

    minimum window substring
  • 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 left does too. So left moves on for good.
  • At least rules (sum at least target): once valid, longer windows can't beat it. So left is 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

nums (6)
2
1
5
1
3
2
▲ i
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
  1. Build the first window of size 3 from scratch, the only time we add up k values. Add 2: sum = 2.
  2. Add 1: sum = 3.
  3. Add 5: sum = 8.
  4. The first full window [2, 1, 5] sums to 8. It's the best so far.
  5. Slide right by one: 1 enters and 2 leaves, so sum = 8 + 1 - 2 = 7. Two operations instead of re-adding 3 values.
  6. 7 doesn't beat 8, so best stays.
  7. Slide right by one: 3 enters and 1 leaves, so sum = 7 + 3 - 1 = 9. Two operations instead of re-adding 3 values.
  8. 9 beats the previous best: best = 9.
  9. Slide right by one: 2 enters and 5 leaves, so sum = 9 + 2 - 5 = 6. Two operations instead of re-adding 3 values.
  10. 6 doesn't beat 9, so best stays.
  11. 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:

  1. Check that a window fits

    Negative numbers fail: use Prefix Sums.

  2. Choose the window's state

    The smallest summary that answers "valid?" in O(1): a sum, or a Map of counts.

  3. Write add(right) and remove(left)

    remove must undo add.

  4. Decide when left moves

    Longest: while invalid. Shortest: while valid.

  5. Record at the right moment

    Longest: after the shrink loop. Shortest: inside it.

Interactive

The sliding window

Grow right, snap left to keep letters unique. Or slide a fixed window of k.

d0v1d2f3a4b5c6d7a8letters in the window0 edge movesbrute force 45 substrings

R grows the window one letter at a time. When a letter repeats, L snaps forward until the repeat is gone. Press Run or Step.

Type your own letters

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

s (7)
a
b
c
b
b
c
a
▲ left
count (size 0)
empty
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
  1. 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.
  2. Extend right to 0: 'a' enters. The window now has 1 distinct character.
  3. Valid window "a" has length 1: a new best.
  4. Extend right to 1: 'b' enters. The window now has 2 distinct characters.
  5. Valid window "ab" has length 2: a new best.
  6. Extend right to 2: 'c' enters. The window now has 3 distinct characters.
  7. 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.
  8. 'a' leaves, its count drops to 0 and it is deleted. Down to 2 distinct: valid again.
  9. Valid window "bc" has length 2; best stays 2.
  10. Extend right to 3: 'b' enters. The window now has 2 distinct characters.
  11. Valid window "bcb" has length 3: a new best.
  12. Extend right to 4: 'b' enters. The window now has 2 distinct characters.
  13. Valid window "bcbb" has length 4: a new best.
  14. Extend right to 5: 'c' enters. The window now has 2 distinct characters.
  15. Valid window "bcbbc" has length 5: a new best.
  16. Extend right to 6: 'a' enters. The window now has 3 distinct characters.
  17. 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.
  18. 'b' leaves (2 still inside). Still 3 distinct: keep shrinking.
  19. 'c' leaves (1 still inside). Still 3 distinct: keep shrinking.
  20. 'b' leaves (1 still inside). Still 3 distinct: keep shrinking.
  21. 'b' leaves, its count drops to 0 and it is deleted. Down to 2 distinct: valid again.
  22. Valid window "ca" has length 2; best stays 5.
  23. 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

nums (7)
5
3
4
1
6
2
2
deque (value@index) (0)
empty
out (0)
empty
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
  1. The deque holds indices whose values decrease from front to back. Its front is always the current window's maximum.
  2. Push index 0 (value 5) at the back. Values in the deque still decrease from front to back.
  3. Push index 1 (value 3) at the back. Values in the deque still decrease from front to back.
  4. 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.
  5. Push index 2 (value 4) at the back. Values in the deque still decrease from front to back.
  6. The window 0..2 is full. Its maximum is the front of the deque: 5.
  7. Index 0 (value 5) is no longer inside the window 1..3: drop it from the front.
  8. Push index 3 (value 1) at the back. Values in the deque still decrease from front to back.
  9. The window 1..3 is full. Its maximum is the front of the deque: 4.
  10. 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.
  11. Push index 4 (value 6) at the back. Values in the deque still decrease from front to back.
  12. The window 2..4 is full. Its maximum is the front of the deque: 6.
  13. Push index 5 (value 2) at the back. Values in the deque still decrease from front to back.
  14. The window 3..5 is full. Its maximum is the front of the deque: 6.
  15. 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.
  16. Push index 6 (value 2) at the back. Values in the deque still decrease from front to back.
  17. The window 4..6 is full. Its maximum is the front of the deque: 6.
  18. 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

try-it.js
// 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
VariantTimeExtra space
Fixed windowO(n)O(1) for a sum
Longest or shortest valid windowO(n)O(a) for an alphabet of size a
Monotonic dequeO(n)O(k)
Brute forceO(n · k) or O(n²)O(1)

JavaScript you'll use

JavaScript refresher: Map and Set Strings and characters Stacks, queues and deques
Read 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 3

Score 2 out of 3 or better to mark this page complete.

1. In the longest-valid-window template, when do you record right - left + 1 as a candidate answer?

Practice problems

Solve these in the browser editor. Each runs against hidden tests; the step-by-step solution unlocks after a wrong submission.

Open these in the problem list →

In real systems

This pattern is not only for coding rounds. Here is where it does the work in a design interview.

Next: Windows answer questions about one range at a time; prefix sums answer any range in O(1).