GUIDE · Patterns
Sliding Window
maintain a contiguous [l, r] range and its running state; move r forward, l forward only.
At a glance
- WHAT
- maintain a contiguous [l, r] range and its running state; move r forward, l forward only.
- TELL
- "contiguous subarray / substring", "longest / shortest / count ... with at most k", "window of size k", "anagram / permutation in string", "minimum window".
- SAY
- fixed-size windowvariable-size windowexpand right / shrink leftwindow invariantfrequency map"need / have" countersmonotonic deque (for window max/min)
- BIG-O
- O(n) time (each index enters and leaves once); O(k) or O(alphabet) space.
- NOTE
- two shapes. FIXED window (size k is given: Template A, sections 1, 5, 7) works on any numbers, negatives included. VARIABLE window on a target sum (Template C, "shortest sum >= target") needs every number >= 0; one negative breaks the shrink step (section 0 shows it). Negatives in "subarray sum = k" -> use prefix-sum map.
The idea#
Picture a train window. You sit facing the countryside, and the window shows one contiguous stretch of it. The right edge of the window only moves forward, as the train does. The left edge only moves forward too. You never slide the window backward. What you can see — the sum of the fields, the set of letters on the billboards, the number of zeros — is the state you keep. When the right edge takes in a new field, you add it. When the left edge leaves a field behind, you drop it. You do not rescan the fields still in the window.
Two kinds of window share that train. A fixed window is a frame of size k. You add the new right edge and, once the frame is full, drop the element that just fell out on the left. A variable window grows until it breaks a rule, then the left edge catches up until the rule holds again. Template B wants the longest valid stretch, so you shrink while the state is invalid and then record the length. Template C wants the shortest valid stretch, so you record the length while the state is valid, and you record it before you shrink.
The rule that makes the left edge safe to move only forward is monotone state. Growing the window should only add information you can undo by dropping the left side. A target sum that includes negative numbers breaks that. Those problems belong with a prefix-sum map.
Why it matters#
You reach for the window when the problem says contiguous subarray or substring, longest or shortest or a count with at most k, a window of size k, an anagram or permutation inside a string, or a minimum window. The nested loop that tries every start and every end is quadratic. The train replaces it when each index enters once and leaves once.
That is O(n) time. The extra space is O(k) for a frequency map, or O(1) when the alphabet is fixed, like 26 letters.
A small example#
function maxSumK(nums, k) { let sum = 0; let best = -Infinity; for (let r = 0; r < nums.length; r++) { sum += nums[r]; if (r >= k) sum -= nums[r - k]; if (r >= k - 1) best = Math.max(best, sum); } return best;} const best = maxSumK([2, 1, 5, 1, 3, 2], 3);That is Template A. The frame slides across [2, 1, 5], [1, 5, 1], [5, 1, 3], [1, 3, 2]. best is 9. Each add and each drop is O(1), so the ride is linear.
What the code below is doing#
The sheet writes the three templates before the problems. Template A is the fixed frame of size k: add the right, drop the left once r >= k, record once the frame is full. Template B is the longest valid window: shrink while invalid, then take the max length. Template C is the shortest valid window: while the state is valid, record the length, then shrink. Recording happens before the shrink, or you lose the window you just proved.
Max sum of size k is Template A with a running sum. Longest substring without repeating characters keeps a set or a map of last indexes and shrinks while the new character is a duplicate. Longest substring with at most k distinct characters keeps a frequency map and shrinks while the map's size is over k. Minimum window substring is Template C with a need-map and a "formed" count: you have matched every required character when formed === required, and you shrink while that stays true. Find-all-anagrams is a fixed window the length of the pattern, comparing length-26 count arrays as the frame moves.
Max consecutive ones, with at most k zeros flipped, counts zeros inside the window and shrinks while that count exceeds k. Sliding-window maximum keeps a deque of indexes whose values are decreasing. The front is the max. You pop the front when it falls out of the frame, and you pop the back while it is smaller than the new value. Each index enters and leaves the deque once, so the whole ride stays linear.
When you talk about it, say this: the right edge only moves forward, the left edge only moves forward, and name which template is driving.
Everything below is the demonstration. Same idea, written as code you can run.
'use strict';const assert = require('node:assert/strict');TEMPLATE A: fixed window of size k#
// for r in 0..n-1:// add nums[r] to state// if r >= k: remove nums[r-k] from state // window is now exactly [r-k+1, r]// if r >= k-1: best = f(best, state)//TEMPLATE B: variable window (longest valid)#
// l = 0// for r in 0..n-1:// add s[r] to state// while state is INVALID: remove s[l]; l++ // shrink until valid again// best = max(best, r - l + 1) // window [l, r] is valid//TEMPLATE C: variable window (shortest valid)#
// for r: add s[r]// while state is VALID: best = min(best, r-l+1); remove s[l]; l++ // record BEFORE shrinking// PRECONDITION for a target sum: every number >= 0 (section 0)The precondition#
Template C on a target sum needs every number >= 0; a brute-force oracle shows the break
// If you came from Session 9.3: this is the case your oracle test catches. The shrink step assumes that// dropping a number from the left can only lower the sum. One negative number makes that false.function shortestSumAtLeast(nums, target) { // Template C, as written for non-negative input let l = 0, sum = 0, best = Infinity; for (let r = 0; r < nums.length; r++) { sum += nums[r]; while (sum >= target) { best = Math.min(best, r - l + 1); sum -= nums[l++]; } } return best === Infinity ? 0 : best;}function shortestSumAtLeastSlow(nums, target) { // the oracle: every [l, r], no assumptions let best = Infinity; for (let l = 0; l < nums.length; l++) { let sum = 0; for (let r = l; r < nums.length; r++) { sum += nums[r]; if (sum >= target) best = Math.min(best, r - l + 1); } } return best === Infinity ? 0 : best;}// at r = 2 the sum hit 5, it dropped the 3, the sum fell to 2, and shrinking stopped. The -2 stays in the window// for good because the loop only shrinks while the sum is still >= target. Negatives: use a prefix-sum map instead.//// From Session 9.3, stop here: Template A, Template C and this section are the visit. Sections 1 and 2 are the two// shapes you already wrote; sections 3 to 7 are interview practice for after Session 9.4.Verified by 4 asserts
assert.equal(shortestSumAtLeast([2, 3, 1, 2, 4, 3], 7), 2); // all >= 0: [4, 3]assert.equal(shortestSumAtLeastSlow([2, 3, 1, 2, 4, 3], 7), 2); // the oracle agreesassert.equal(shortestSumAtLeastSlow([3, -2, 4, 1], 5), 2); // one negative: the true answer is [4, 1]assert.equal(shortestSumAtLeast([3, -2, 4, 1], 5), 3); // the window says [3, -2, 4] and never sees [4, 1]:Max sum subarray of size k (fixed)#
add right, drop left
function maxSumK(nums, k) { if (nums.length < k) return 0; let sum = 0, best = -Infinity; for (let r = 0; r < nums.length; r++) { sum += nums[r]; if (r >= k) sum -= nums[r - k]; if (r >= k - 1) best = Math.max(best, sum); } return best;}Verified by 3 asserts
assert.equal(maxSumK([2, 1, 5, 1, 3, 2], 3), 9);assert.equal(maxSumK([-1, -2, -3], 2), -3);assert.equal(maxSumK([1], 2), 0);Longest substring without repeating chars (variable, longest)#
Set or last-index map; shrink while dup
function lengthOfLongestSubstring(s) { const seen = new Set(); let l = 0, best = 0; for (let r = 0; r < s.length; r++) { while (seen.has(s[r])) seen.delete(s[l++]); seen.add(s[r]); best = Math.max(best, r - l + 1); } return best;}Verified by 3 asserts
assert.equal(lengthOfLongestSubstring('abcabcbb'), 3);assert.equal(lengthOfLongestSubstring('bbbb'), 1);assert.equal(lengthOfLongestSubstring(''), 0);Longest substring with at most k distinct#
freq map; shrink while map.size > k
function longestKDistinct(s, k) { const f = new Map(); let l = 0, best = 0; for (let r = 0; r < s.length; r++) { f.set(s[r], (f.get(s[r]) ?? 0) + 1); while (f.size > k) { f.set(s[l], f.get(s[l]) - 1); if (f.get(s[l]) === 0) f.delete(s[l]); l++; } best = Math.max(best, r - l + 1); } return best;}Verified by 3 asserts
assert.equal(longestKDistinct('eceba', 2), 3);assert.equal(longestKDistinct('aa', 1), 2);assert.equal(longestKDistinct('abc', 0), 0);Minimum window substring (variable, shortest)#
need map + "formed" count; shrink while formed === required
function minWindow(s, t) { const need = new Map(); for (const c of t) need.set(c, (need.get(c) ?? 0) + 1); let have = 0, l = 0, best = [0, Infinity]; const win = new Map(); for (let r = 0; r < s.length; r++) { const c = s[r]; win.set(c, (win.get(c) ?? 0) + 1); if (need.has(c) && win.get(c) === need.get(c)) have++; while (have === need.size) { if (r - l < best[1] - best[0]) best = [l, r]; const d = s[l++]; win.set(d, win.get(d) - 1); if (need.has(d) && win.get(d) < need.get(d)) have--; } } return best[1] === Infinity ? '' : s.slice(best[0], best[1] + 1);}Verified by 3 asserts
assert.equal(minWindow('ADOBECODEBANC', 'ABC'), 'BANC');assert.equal(minWindow('a', 'a'), 'a');assert.equal(minWindow('a', 'aa'), '');Find all anagrams / permutation in string (fixed window = |p|)#
26-count arrays; compare per step
function findAnagrams(s, p) { const out = [], need = new Array(26).fill(0), win = new Array(26).fill(0); const ix = (c) => c.charCodeAt(0) - 97; for (const c of p) need[ix(c)]++; for (let r = 0; r < s.length; r++) { win[ix(s[r])]++; if (r >= p.length) win[ix(s[r - p.length])]--; if (r >= p.length - 1 && win.every((v, i) => v === need[i])) out.push(r - p.length + 1); } return out;}const checkInclusion = (p, s) => findAnagrams(s, p).length > 0; // "permutation in string"Verified by 3 asserts
assert.deepEqual(findAnagrams('cbaebabacd', 'abc'), [0, 6]);assert.deepEqual(findAnagrams('abab', 'ab'), [0, 1, 2]);assert.equal(checkInclusion('ab', 'eidboaoo'), false);Max consecutive ones III (flip at most k zeros)#
count zeros in window; shrink while zeros > k
function longestOnes(nums, k) { let l = 0, zeros = 0, best = 0; for (let r = 0; r < nums.length; r++) { if (nums[r] === 0) zeros++; while (zeros > k) if (nums[l++] === 0) zeros--; best = Math.max(best, r - l + 1); } return best;}Verified by 3 asserts
assert.equal(longestOnes([1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0], 2), 6);assert.equal(longestOnes([0, 0, 0], 0), 0);assert.equal(longestOnes([1, 1], 5), 2);Sliding window maximum (monotonic deque)#
deque of indices, values decreasing; front = max; pop front when out of window
function maxSlidingWindow(nums, k) { const dq = []; // indices; nums[dq] strictly decreasing front->back let head = 0; // head index instead of shift() to keep O(1) const out = []; for (let r = 0; r < nums.length; r++) { while (dq.length > head && nums[dq[dq.length - 1]] <= nums[r]) dq.pop(); // drop smaller tail dq.push(r); if (dq[head] <= r - k) head++; // front fell out of the window if (r >= k - 1) out.push(nums[dq[head]]); } return out;}Verified by 3 asserts
assert.deepEqual(maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3), [3, 3, 5, 5, 6, 7]);assert.deepEqual(maxSlidingWindow([1], 1), [1]);assert.deepEqual(maxSlidingWindow([4, 4, 4], 2), [4, 4]);See also