Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Beginner/B7
7 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣB7 · Part 1 · Beginner

Sliding window

Stop re-scanning the same elements — slide the window instead.

22 min read1 diagram17 sections19 exercises
7 of 48

In this chapter

  • The insight: don't recompute, adjust
  • Fixed-size window
  • Variable-size window — the more common interview shape
  • The general variable-window template
  • See the window move

and 12 more

about 22 min
17On this page
  1. The insight: don't recompute, adjust
  2. Fixed-size window
  3. Variable-size window — the more common interview shape
  4. The general variable-window template
  5. See the window move
  6. Longest or shortest: where the answer is recorded
  7. Counting windows: every right end adds right − left + 1
  8. At most K distinct: a longest window over a counting map
  9. Exactly K is at most K minus at most K − 1
  10. The need and missing counters: minimum window substring
  11. Fixed window with a frequency compare: anagrams and permutations
  12. A budget window: replace K characters, flip K zeros
  13. When the window breaks: negative numbers and non-monotone rules
  14. Sliding window maximum: a window with a deque
  15. Which template for which question
  16. Pitfalls that cost offers
  17. Recognizing it in an unseen problem

The insight: don't recompute, adjust

A brute-force "every contiguous subarray/substring" solution recomputes each window from scratch — O(n) work, done for O(n) windows, is O(n²). Sliding window notices that consecutive windows overlap almost entirely: moving from [i, j] to [i+1, j+1] only removes one element and adds one element. Update the running answer instead of rebuilding it, and the whole scan collapses to O(n).

3 1 4 1 5 9 window, size 3 slide right one step: drop nums[left], add nums[right+1] O(1) update, not a rescan
The window's total is maintained incrementally — never recomputed from scratch.

Fixed-size window

// max sum of any window of size k — O(n) time, O(1) space
function maxSumWindow(nums, k) {
  if (k < 1 || k > nums.length) return null; // no window of that size exists; 0 would look like a real sum
  let windowSum = 0;
  for (let i = 0; i < k; i++) windowSum += nums[i]; // build first window

  let best = windowSum;
  for (let i = k; i < nums.length; i++) {
    windowSum += nums[i] - nums[i - k]; // add new, drop old — O(1)
    best = Math.max(best, windowSum);
  }
  return best;
}

Variable-size window — the more common interview shape

Here the window grows on the right and shrinks from the left based on a condition, instead of staying a fixed size. This is the pattern behind "longest substring without repeating characters," "smallest subarray with sum ≥ target," and most "longest/shortest X satisfying Y" questions.

// longest substring with no repeated characters — O(n) time, O(min(n, alphabet)) space
function longestUniqueSubstring(s) {
  const lastSeen = new Map(); // char → most recent index
  let left = 0, best = 0;

  for (let right = 0; right < s.length; right++) {
    const c = s[right];
    if (lastSeen.has(c) && lastSeen.get(c) >= left) {
      left = lastSeen.get(c) + 1; // jump left past the repeat
    }
    lastSeen.set(c, right);
    best = Math.max(best, right - left + 1);
  }
  return best;
}

Notice left only ever moves forward — it never resets to 0 and re-scans. That "each pointer visits each index at most once" property is why this is O(n) and not O(n²) despite looking like a nested loop conceptually.

Dry run: longestUniqueSubstring("abcabcbb")

rightcharrepeat in window?left jumps towindowbest
0ano0"a"1
1bno0"ab"2
2cno0"abc"3
3ayes (index 0)1"bca"3
4byes (index 1)2"cab"3
5cyes (index 2)3"abc"3
6byes (index 4)5"cb"3
7byes (index 6)7"b"3

left jumps straight to one past the repeat's last position — never one step at a time, never backward. Across all 8 steps, left moved a total of 7 positions, not 7 positions per step — that's the amortized O(n) at work.

The general variable-window template

function template(arr, condition) {
  let left = 0;
  let state = /* running total, count, or map */ 0;

  for (let right = 0; right < arr.length; right++) {
    // 1. expand: fold arr[right] into state

    while (/* state violates the condition */ false) {
      // 2. shrink: undo arr[left] from state, then left++
      left++;
    }

    // 3. update the answer using the current valid window [left, right]
  }
}
⚠ Why the shrink loop doesn't make this O(n²) It looks like a loop inside a loop, but left only ever increases and can move at most n times total across the whole run — not n times per iteration of the outer loop. Add the outer loop's n steps and the inner loop's n total steps together (not multiply) and you get O(2n) = O(n). This "amortized" argument is worth being able to say out loud in an interview.
Say it like this → "I'll maintain a window with two pointers instead of recomputing each substring — the right pointer expands the window, the left pointer only shrinks it when the condition breaks, so each index is visited a constant number of times total, giving O(n) instead of the O(n²) brute force."

See the window move

Watch left jump rather than crawl. That jump is what keeps the whole scan linear even though the window shrinks and grows.

Sliding window — longest substring without repeats

Window
Best so far

Longest or shortest: where the answer is recorded

The template above has one open slot: "update the answer using the current valid window." Where you fill it depends on whether you want the longest or the shortest valid window, and getting it wrong is the most common way to write a window that looks right and is off by one.

  • Longest valid window. Expand, then shrink until the window is valid again, then record. The window is valid at the moment you read its length, and it is as large as it can be for this right end.
  • Shortest valid window. Expand until the window becomes valid, then record inside the shrink loop and keep shrinking for as long as it stays valid. Every valid window you pass on the way down is a candidate, and the last one is the shortest for this right end.

The classic shortest case is the minimal-length subarray whose sum is at least target, for an array of positive numbers.

function minSubArrayLen(target, nums) {
  let left = 0, sum = 0, best = Infinity;
  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];
    while (sum >= target) {                       // valid: record, then try a shorter one
      best = Math.min(best, right - left + 1);
      sum -= nums[left++];
    }
  }
  return best === Infinity ? 0 : best;            // no valid window: 0, not Infinity
}

Dry run: minSubArrayLen(7, [2, 3, 1, 2, 4, 3])

rightaddedwindows recorded in the shrink loopsum afterleft afterbest
02none (sum 2)20∞
13none (sum 5)50∞
21none (sum 6)60∞
32[2, 3, 1, 2] length 4614
44[3, 1, 2, 4] length 4, then [1, 2, 4] length 3633
53[2, 4, 3] length 3, then [4, 3] length 2352

Why left never has to move backward: with positive numbers, if the window [l, r] already reaches the target, so does [l, r + 1]. The latest start that still reaches the target (the one that gives the shortest window for that right end) can only move right as right grows, so each index is added once and removed at most once. Time O(n), space O(1).

⚠ Three slips in the shortest-window shape
  • Using if instead of while to shrink records one window per right end and misses shorter ones that appear after several removals.
  • Recording after the loop, when the window has just become invalid, reports a window that does not satisfy the rule.
  • Returning Infinity (or a stale length) when no window qualifies. Decide what "none" looks like before you write the loop.

Counting windows: every right end adds right − left + 1

Some questions ask how many subarrays satisfy a rule, not how long the best one is. Take: how many contiguous subarrays of a positive-integer array have a product strictly less than k?

Fix the right end and let left be the smallest start that keeps the product below k. Shrinking a window of positive numbers never increases its product, so every start from left to right is also valid, and no start before left is. That is exactly right - left + 1 valid subarrays ending at right. Add that number at each step and every valid subarray is counted exactly once, grouped by where it ends.

function numSubarrayProductLessThanK(nums, k) {
  if (k <= 1) return 0;                          // products of positive integers are at least 1
  let left = 0, product = 1, count = 0;
  for (let right = 0; right < nums.length; right++) {
    product *= nums[right];
    while (product >= k) product /= nums[left++];
    count += right - left + 1;                    // all starts in [left, right] work
  }
  return count;
}
right (value)product after shrinkleftnew subarraystotal
0 (10)1001: [10]1
1 (5)5002: [5], [10, 5]3
2 (2)10 (100 reached the limit, so the 10 is removed)12: [2], [5, 2]5
3 (6)6013: [6], [2, 6], [5, 2, 6]8

This is k = 100 on [10, 5, 2, 6]. Time O(n), space O(1). The k <= 1 guard is not decoration: with k = 1 the loop condition stays true after every element has been divided out, left runs past the end of the array, and the count comes out wrong (-1 for [5] with k = 1). The division is exact here because product is always a product of the numbers in the window, as long as it stays below 253.

At most K distinct: a longest window over a counting map

"Longest substring with at most K distinct characters" (and "fruit into baskets", which is the same question with K = 2) needs the window to know how many of each thing it holds, not just a total. A Map from item to count does it, and its size is the number of distinct items.

function longestAtMostKDistinct(s, k) {
  const counts = new Map();
  let left = 0, best = 0;
  for (let right = 0; right < s.length; right++) {
    counts.set(s[right], (counts.get(s[right]) || 0) + 1);
    while (counts.size > k) {
      const c = s[left++];
      counts.set(c, counts.get(c) - 1);
      if (counts.get(c) === 0) counts.delete(c);  // without this, size never drops
    }
    best = Math.max(best, right - left + 1);
  }
  return best;
}
rightcharcounts afterwindowbest
0ee:1"e"1
1ce:1, c:1"ec"2
2ee:2, c:1"ece"3
3bthree distinct, so drop e, then c: e:1, b:1"eb"3
4athree distinct, so drop e: b:1, a:1"ba"3

This is k = 2 on "eceba". Each character enters once and leaves at most once: O(n) time, and the map holds at most k + 1 keys, so O(k) space. With k = 0 the shrink loop empties the window every time and the answer is 0, which is correct for an empty allowance. The "fruit" version is the same code over an array of numbers.

Exactly K is at most K minus at most K − 1

Now ask for the number of subarrays with exactly K distinct values. The window machinery cannot answer this directly, because "exactly K" is not monotone: a window with exactly K distinct values can lose one when you shrink it, and gain one when you expand it, so neither pointer has a reliable direction. "At most K" does behave: shrinking never increases the distinct count, so the valid windows for a given right end are exactly the starts from left to right.

The trick A subarray with at most K distinct values either has at most K − 1, or has exactly K. So exactly(K) = atMost(K) − atMost(K − 1). Each term is a plain counting window, and the difference leaves precisely the subarrays you want.
function subarraysWithKDistinct(nums, k) {
  return atMostKDistinct(nums, k) - atMostKDistinct(nums, k - 1);
}

function atMostKDistinct(nums, k) {
  const counts = new Map();
  let left = 0, total = 0;
  for (let right = 0; right < nums.length; right++) {
    counts.set(nums[right], (counts.get(nums[right]) || 0) + 1);
    while (counts.size > k) {
      const x = nums[left++];
      counts.set(x, counts.get(x) - 1);
      if (counts.get(x) === 0) counts.delete(x);
    }
    total += right - left + 1;
  }
  return total;
}
rightat most 2: windowaddstotalat most 1: windowaddstotal
0[1]11[1]11
1[1, 2]23[2]12
2[1, 2, 1]36[1]13
3[1, 2, 1, 2]410[2]14
4[2, 3]212[3]15

On [1, 2, 1, 2, 3] with K = 2: 12 − 5 = 7 subarrays have exactly two distinct values. Two linear passes, so O(n) time. The same subtraction counts subarrays whose sum equals a goal when the numbers are non-negative: atMostSum(goal) - atMostSum(goal - 1), and "nice" subarrays with exactly K odd numbers once each element is replaced by x % 2.

function numSubarraysWithSum(nums, goal) {
  return atMostSum(nums, goal) - atMostSum(nums, goal - 1);
}

function atMostSum(nums, s) {
  if (s < 0) return 0;                          // no non-negative sum is below zero
  let left = 0, sum = 0, total = 0;
  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];
    while (sum > s) sum -= nums[left++];
    total += right - left + 1;
  }
  return total;
}
Say it like this → "Exactly K isn't monotone, so a single window can't track it. At most K is, so I'll count subarrays with at most K and subtract those with at most K minus one; what's left is exactly K, in two linear passes."

The need and missing counters: minimum window substring

Given strings s and t, find the shortest window of s that contains every character of t, counting repeats: if t has two As, the window needs two. This is the template's hardest everyday use, and it is easiest if you keep one idea in your head.

For each character, store need[c] = how many copies of c the window still lacks. It starts at the count in t. Adding a character to the window subtracts one; removing one adds one. A positive value means the window is still short of that character; zero or below means it has enough, and a negative number is a surplus. To avoid scanning the whole map after every step, keep one more number, missing: the total of the positive parts, starting at t.length. The window is valid exactly when missing === 0.

function minWindow(s, t) {
  if (t.length === 0 || t.length > s.length) return "";
  const need = new Map();
  for (const c of t) need.set(c, (need.get(c) || 0) + 1);

  let missing = t.length;
  let left = 0, bestStart = 0, bestLen = Infinity;

  for (let right = 0; right < s.length; right++) {
    const c = s[right];
    if (need.has(c)) {
      if (need.get(c) > 0) missing--;            // this copy was still needed
      need.set(c, need.get(c) - 1);
    }
    while (missing === 0) {                       // valid: record, then try to shrink
      if (right - left + 1 < bestLen) {
        bestLen = right - left + 1;
        bestStart = left;
      }
      const d = s[left++];
      if (need.has(d)) {
        need.set(d, need.get(d) + 1);
        if (need.get(d) > 0) missing++;          // we just gave up a copy we needed
      }
    }
  }
  return bestLen === Infinity ? "" : s.slice(bestStart, bestStart + bestLen);
}

Dry run: minWindow("ADOBECODEBANC", "ABC")

rightcharmissingwhat the shrink loop doesleft afterbest so far
0–2A, D, O2nothing; A was needed, D and O are not in t0none
3B1nothing0none
4E1nothing0none
5C0"ADOBEC" is valid: record 6, drop A, missing is 1 again1"ADOBEC"
6–9O, D, E, B1nothing (a second B is a surplus, not a fix)1"ADOBEC"
10A0valid again: the window stays valid at lengths 10, 9, 8, 7, 6 as D, O, B, E, C are dropped, but none beats 6; dropping C breaks it6"ADOBEC"
11N1nothing6"ADOBEC"
12C0valid at lengths 7, 6, 5 and 4 ("BANC") as O, D, E, B are dropped; dropping B breaks it10"BANC"

Each index enters the window once and leaves at most once, and each step is an O(1) map operation: O(|s| + |t|) time. The map holds only the distinct characters of t, so the space is O(number of distinct characters in t). Because the comparison is strict, ties keep the first shortest window.

⚠ Where this goes wrong
  • Decrementing missing on every matching character, instead of only while need[c] > 0. A third A in the window is a surplus and must not count.
  • Ignoring characters that are not in t is correct, but forgetting the need.has(c) check puts them in the map with negative counts, and then removing them can push missing up wrongly.
  • Comparing need.size or the map's contents on every step instead of using missing. It still works but turns O(n) into O(n · distinct).
  • Duplicates in t: "AA" needs two As, not just the presence of A.

Fixed window with a frequency compare: anagrams and permutations

"Find every start index where an anagram of p appears in s", and "does s2 contain a permutation of s1", are fixed windows of length k = p.length. The window is valid when its character counts equal the counts of p. The code assumes lowercase letters, to keep the indexing simple. Comparing two 26-slot arrays each step costs O(26) per step, which is fine, but you can make each slide O(1) by tracking only the difference and a counter of how many letters still differ.

function findAnagrams(s, p) {
  const out = [];
  const k = p.length;
  if (k > s.length) return out;
  const diff = new Array(26).fill(0);           // window count minus pattern count, per letter
  let unequal = 0;                              // how many letters have diff !== 0

  function bump(ch, delta) {
    const i = ch.charCodeAt(0) - 97;
    if (diff[i] === 0) unequal++;
    diff[i] += delta;
    if (diff[i] === 0) unequal--;
  }

  for (const ch of p) bump(ch, -1);
  for (let right = 0; right < s.length; right++) {
    bump(s[right], 1);
    if (right >= k) bump(s[right - k], -1);     // the letter that just left the window
    if (right >= k - 1 && unequal === 0) out.push(right - k + 1);
  }
  return out;
}

function checkInclusion(s1, s2) {
  return findAnagrams(s2, s1).length > 0;
}
rightwindow (size 3)letters still differentresult
0"c" (filling)2—
1"cb" (filling)1—
2"cba"0match at 0
3–7"bae", "aeb", "eba", "bab", "aba"2no match
8"bac"0match at 6
9"acd"2no match

This is s = "cbaebabacd" and p = "abc", which gives [0, 6]. Time O(|s| + |p|) and space O(1) for 26 lowercase letters. For a larger alphabet, use a Map and delete entries whose count reaches zero, and keep the same counter idea. An anagram and a permutation are the same thing here, since both mean "same letters, same counts, any order"; a sorted copy or a fresh count at each window would also work but costs O(k) per window.

A budget window: replace K characters, flip K zeros

A third kind of validity is a budget: the window may contain at most K things that are "wrong" and need fixing. Max consecutive ones III allows flipping at most K zeros, so a window is valid when it contains at most K zeros. That is the "at most" window again, with a counter instead of a map.

function longestOnes(nums, k) {
  let left = 0, zeros = 0, best = 0;
  for (let right = 0; right < nums.length; right++) {
    if (nums[right] === 0) zeros++;
    while (zeros > k) {
      if (nums[left] === 0) zeros--;
      left++;
    }
    best = Math.max(best, right - left + 1);
  }
  return best;
}

Longest repeating character replacement is the same budget with a twist. You may replace up to K characters so the window becomes one repeated letter. In a window of length len, you keep the most frequent letter and replace the rest, which costs len - maxCount replacements. The window is valid when that cost is at most K.

function characterReplacement(s, k) {
  const counts = new Array(26).fill(0);        // uppercase A-Z
  let left = 0, best = 0;
  for (let right = 0; right < s.length; right++) {
    counts[s.charCodeAt(right) - 65]++;
    while (right - left + 1 - Math.max(...counts) > k) {
      counts[s.charCodeAt(left) - 65]--;
      left++;
    }
    best = Math.max(best, right - left + 1);
  }
  return best;
}
rightwindow after shrinkingmost frequent countlength − most frequentbest
0"A"101
1"AA"202
2"AAB"213
3"AABA"314
4"BAB" (the 5-letter window needed 2 replacements)214
5"BABB"314
6"BBA"214

This is "AABABBA" with K = 1, answer 4. The Math.max(...counts) scan costs 26 operations, so the total is O(26 · n), which is O(n). A well-known variant keeps maxCount as a running maximum that is never lowered, and replaces the while with an if, so the window never gets smaller. That version is correct and avoids the scan, but its proof takes a paragraph, so write the scanning version first and mention the variant only if asked about the constant factor.

When the window breaks: negative numbers and non-monotone rules

Every window above rests on one property: validity is monotone. If a window is valid, so are the windows you get by shrinking it (for the "at most" rules), or growing it (for the "at least" rules), and moving left forward therefore never throws away an answer. Lose the property and the pointers stop knowing which way to go.

The usual way to lose it is a negative number. Run the shortest-window template on "shortest subarray with sum at least k" for [84, -37, 32, 40, 95] and k = 167:

// WRONG when nums can be negative
function shortestSubarrayWrong(nums, k) {
  let left = 0, sum = 0, best = Infinity;
  for (let right = 0; right < nums.length; right++) {
    sum += nums[right];
    while (sum >= k) {
      best = Math.min(best, right - left + 1);
      sum -= nums[left++];
    }
  }
  return best === Infinity ? -1 : best;
}

It returns 5: at the last element the sum is 214, the whole array is recorded, and removing the 84 drops the sum to 130, below 167, so the loop stops. But the window [32, 40, 95] sums to exactly 167 and has length 3. To reach it you would have to remove the -37 as well, and removing a negative raises the sum, which the loop never tries because it stopped the moment the sum dropped.

RuleMonotone?Works with the window?
Sum at least or at most a target, positive numbersyesyes
Product below k, positive numbersyesyes
At most K distinct, at most K zerosyesyes
Exactly K of anythingnoonly as at most K minus at most K − 1
Sum equal to a target, with negativesnono: use prefix sums and a hash map (hashing)
Shortest sum at least k, with negativesnono: prefix sums plus a monotonic deque (monotonic stack and queue)

The interview habit that saves you: before you commit to a window, ask "if this window is valid, what happens when I add an element, and when I remove one?" If the answer depends on the sign or the value of what you add, stop and reach for prefix sums.

Sliding window maximum: a window with a deque

So far the state could be updated in O(1) in both directions: a sum is undone by subtraction, a count by decrementing. A maximum cannot be undone. When the largest element leaves the window, you need the largest of what is left, and nothing in a single running number tells you that. The brute force rescans each window, O(n · k).

Keep only the elements that could still become the maximum. Suppose a new element x arrives and an earlier element in the window is smaller than or equal to x. That earlier element can never be the maximum again, because x is at least as large and leaves the window later. So throw it away. What survives is a list of indices whose values decrease from front to back, and the front is always the maximum of the window. Two more rules finish it: drop the front when its index falls out of the window, and read the front after each full window. The list needs removal at both ends, which is a deque. (Assume 1 <= k <= nums.length.)

function maxSlidingWindow(nums, k) {
  const out = [];
  const dq = [];            // indices; the live part starts at head, values decrease
  let head = 0;
  for (let i = 0; i < nums.length; i++) {
    while (dq.length > head && nums[dq[dq.length - 1]] <= nums[i]) dq.pop();  // dominated by nums[i]
    dq.push(i);
    if (dq[head] <= i - k) head++;            // the front index left the window
    if (i >= k - 1) out.push(nums[dq[head]]);
  }
  return out;
}

Dry run: maxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3)

i (value)live deque (values)output so far
0 (1)[1][]
1 (3)[3] (1 dominated, removed)[]
2 (−1)[3, −1][3]
3 (−3)[3, −1, −3][3, 3]
4 (5)[5] (all three dominated)[3, 3, 5]
5 (3)[5, 3][3, 3, 5, 5]
6 (6)[6][3, 3, 5, 5, 6]
7 (7)[7][3, 3, 5, 5, 6, 7]

Each index is pushed once and popped at most once, so the loop does O(n) work in total even though the inner while can pop many entries at one step. The live part holds at most k indices. Using a head index avoids shift(), which the language defines as moving every remaining element down one place, so you should not count on it being O(1) when you explain the complexity. Popping on <= rather than < keeps the deque strictly decreasing and is safe for equal values, since the newer one lasts longer. The same two-ended structure answers "longest subarray where max minus min is at most a limit" with one deque for the maximum and one for the minimum, and is developed in monotonic stack and queue.

Which template for which question

The question asks forShapeWhen you record
Longest window that satisfies a ruleexpand, shrink while invalidafter the shrink loop
Shortest window that satisfies a ruleexpand, shrink while validinside the shrink loop, before removing
How many windows satisfy an "at most" ruleexpand, shrink while invalidadd right - left + 1 after the shrink loop
How many windows satisfy "exactly K"two counting windowsatMost(K) − atMost(K − 1)
Contains all of another string, with repeatsneed map plus missinginside the shrink loop while missing === 0
Window of a fixed sizeadd the new, drop the oldonce the window is full
Maximum or minimum of every windowmonotonic dequethe front, once the window is full

Pitfalls that cost offers

  • Negative numbers break any sum-based window. Check the sign constraint in the prompt before writing a line.
  • Substring or subarray, not subsequence. A window is contiguous. If the characters may be skipped, it is a different problem.
  • Forgetting to delete zero-count keys makes map.size lie, so the distinct-count rule never fires.
  • if where while belongs. One removal may not restore validity.
  • Off-by-one in the length. The length of [left, right] is right - left + 1. Writing right - left is the commonest slip, and it is wrong by exactly one on every answer.
  • Recording at the wrong moment. Longest: after shrinking. Shortest: before removing. Counting: after shrinking.
  • Re-initialising instead of sliding. Rebuilding the state for every window is the O(n · k) solution you were meant to replace.
  • Empty input, k larger than the array, k = 0. Decide what each should return before you code, as with maxSumWindow above.

Recognizing it in an unseen problem

  • The words "contiguous subarray" or "substring" (not subsequence)
  • "Longest," "shortest," "maximum," or "minimum" over a contiguous range
  • A brute force would check every [i, j] pair — O(n²) or worse
  • The condition can be checked/updated incrementally as the window changes
  • Adding an element can only push the window toward invalid, and removing one can only push it back (monotone validity); check this before the sign of the numbers
  • "How many subarrays" with an "at most" rule, or "exactly K" that can be rewritten as at most K minus at most K − 1
  • "Contains all the characters of T", "is a permutation of", or "at most K replacements/flips" over a string or a binary array
  • "Maximum or minimum of every window of size k", which needs a monotonic deque rather than a running number
Before you move on
  • Explain why sliding window is O(n) even though the shrink step looks like a loop inside a loop — the amortized argument, not just "it's fast."
  • Trace longestUniqueSubstring on a new string by hand, including at least one point where left jumps more than one position.
  • Write the general variable-window template (expand, shrink while invalid, update answer) from memory.
  • Say why left jumping straight past the repeat, instead of stepping one at a time, still keeps the total work O(n).
  • Given a new problem, recognize a fixed-size window versus a variable-size window, and state which condition would trigger the shrink step.
  • Say where the answer is recorded for the longest window, the shortest window and a count of windows, and why those places differ.
  • Rewrite "exactly K" as at most K minus at most K − 1, and explain why exactly K is not monotone.
  • Write minimum window substring with the need map and the missing counter, including what happens with duplicate characters in t.
  • Give a concrete input where a sum window fails because of a negative number, and name the replacement technique.
  • Explain why a maximum needs a deque while a sum does not, and why each index is popped at most once.

19Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsLongest Substring Without Repeating Charactersintermediate
  • 8 testsMinimum Window Substringadvanced
  • 8 testsFind All Anagrams in a Stringintermediate
  • 8 testsLongest Repeating Character Replacementintermediate
  • 8 testsPermutation in Stringintermediate
  • 8 testsMinimum Size Subarray Sumintermediate
  • 8 testsFruit Into Basketsintermediate
  • 8 testsSubarray Product Less Than Kintermediate
  • 8 testsMax Consecutive Ones IIIintermediate
  • 8 testsLongest Continuous Subarray With Absolute Diff <= Limitintermediate
  • 8 testsMaximum Average Subarraybeginner
  • 8 testsLongest Substring with At Most K Distinct Charactersintermediate
  • 8 testsSubarrays with K Different Integersintermediate
  • 8 testsSubarrays with a Given Sumintermediate
  • 8 testsCount Nice Subarraysintermediate
  • 8 testsMaximum Points You Can Obtain from Cardsintermediate
  • 8 testsGrumpy Bookstore Ownerintermediate
  • 9 testsSliding Window Maximumintermediate
  • 8 testsSubarray Sum Equals Kintermediate
All 217 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

B7

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. B6Previous · B6Two pointers
  2. B7you are here
  3. Next · B8Binary searchHalving the search space is the single highest-leverage trick in DSA.27 min read

01On this page

17 sections · 0%

  1. 01The insight: don't recompute, adjust
  2. 02Fixed-size window
  3. 03Variable-size window — the more common interview shape
  4. 04The general variable-window template
  5. 05See the window move
  6. 06Longest or shortest: where the answer is recorded
  7. 07Counting windows: every right end adds right − left + 1
  8. 08At most K distinct: a longest window over a counting map
  9. 09Exactly K is at most K minus at most K − 1
  10. 10The need and missing counters: minimum window substring
  11. 11Fixed window with a frequency compare: anagrams and permutations
  12. 12A budget window: replace K characters, flip K zeros
  13. 13When the window breaks: negative numbers and non-monotone rules
  14. 14Sliding window maximum: a window with a deque
  15. 15Which template for which question
  16. 16Pitfalls that cost offers
  17. 17Recognizing it in an unseen problem
Mark as read

[] chapters · t top