LC Patterns — Sliding Window

Prev: binary-search, two-pointers · Code: C++
Per-pattern template: What it is → Intuition → Approach → Pitfalls → LC problems → Worked examples

◆ The Master Insight

A sliding window is two chase pointers plus a running summary of whatever sits between them (a sum, a frequency map, a max). The window slides instead of recomputing: an element entering updates the summary in O(1); an element leaving un-does its contribution. Each pointer moves at most n steps → O(n), versus O(n²) re-examining every subarray.

The whole topic is four questions: is the window fixed or variable size? Optimizing for longest, shortest, or a count? What's the summary you can update in O(1)? And is the constraint monotonic?

When it works — monotonicity. Growing the window must push the constraint steadily one way, shrinking must push it back. "Sum ≥ target" with non-negative numbers is fine. Add negatives and it breaks — a longer window can have a smaller sum — and the window silently gives wrong answers; switch to prefix sums + hashmap or a monotonic deque (Pattern 7). Saying this precondition out loud is the senior move of this topic.
Recognition trigger: "longest/shortest/count subarray or substring such that ⟨rule⟩", "contiguous", "window of size k". Contiguity is required — if the problem says subsequence, sliding window is the wrong tool (that's DP or greedy).

§0The Four Templates & Universal Pitfalls

C++ · Template F — fixed size k
long long s = 0;
for (int i = 0; i < k; ++i) s += nums[i];          // warm-up on the first k
int best = s;
for (int r = k; r < n; ++r) {
    s += nums[r] - nums[r - k];                     // slide by one: one in, one out
    best = max(best, (int)s);
}
C++ · Template L — longest window satisfying constraint
int l = 0, best = 0;
for (int r = 0; r < n; ++r) {
    add(nums[r]);                                   // 1. expand
    while (invalid()) remove(nums[l++]);            // 2. shrink until valid again
    best = max(best, r - l + 1);                    // 3. window is valid here
}
C++ · Template S — shortest window satisfying constraint
int l = 0, best = INT_MAX;
for (int r = 0; r < n; ++r) {
    add(nums[r]);
    while (valid()) {                               // shrink while STILL valid
        best = min(best, r - l + 1);                // record BEFORE breaking validity
        remove(nums[l++]);
    }
}
C++ · Template C — counting subarrays
int l = 0; long long count = 0;
for (int r = 0; r < n; ++r) {
    add(nums[r]);
    while (invalid()) remove(nums[l++]);
    count += r - l + 1;                             // every left in [l,r] gives a valid window ending at r
}
Mirror image: longest shrinks while invalid and records after; shortest shrinks while valid and records inside the shrink loop. If your two templates look identical, one is wrong.

Universal pitfalls

1. Sum-window on negatives. The constraint isn't monotonic → the shrink logic is unsound. Ask "can values be negative?" before committing.

2. Update order. add(nums[r]) before the validity check; the window you record is r − l + 1 after shrinking.

3. while vs if when shrinking. One new element can break validity by more than one step, so shrinking needs while.

4. Non-O(1) summary. "Max of the window" can't be un-done by subtraction — removing the max needs the runner-up → a monotonic deque (Pattern 7). An O(window) remove() is O(n²) in disguise.

5. Stale zeros in a freq map. A count left at 0 breaks whole-map comparisons — track a matched/need counter instead of comparing maps.

6. Off-by-one on size. Window [l, r] has length r − l + 1. Write it once as a comment — half of all window bugs are this.

1Fixed-Size Window, Numeric Summary

What it is: The window size k is given — compute something (max average, count of vowels, threshold checks) over every contiguous block of exactly k elements. Slide by one: add the element entering, subtract the one leaving, never recompute the whole block.

Max Average Subarray I (LC 643) — nums = [1,12,−5,−6,50,3], k = 4
window 1 · sum = 2
1
12
−5
−6
50
3
slide → +50 −1 · sum = 2 + 50 − 1 = 51
1
12
−5
−6
50
3
slide → +3 −12 · sum = 51 + 3 − 12 = 42
1
12
−5
−6
50
3
best average = 51 / 4 = 12.75. Each slide is two arithmetic ops, not a re-sum of four elements.

Intuition: two neighbouring windows share k−1 elements — recomputing throws that overlap away. The delta (one in, one out) is the whole idea. This is the "hello world" of the topic: drill the slide mechanics here so the variable-size patterns are pure thinking.

C++ · Max Average Subarray I (LC 643)
double findMaxAverage(vector<int>& nums, int k) {
    double s = 0;
    for (int i = 0; i < k; ++i) s += nums[i];             // warm-up on first k
    double best = s;
    for (int r = k; r < (int)nums.size(); ++r) {
        s += nums[r] - nums[r - k];                       // one in, one out
        best = max(best, s);
    }
    return best / k;
}

Challenges & pitfalls

Warm-up loop boundaries; forgetting the remove side of the slide; integer division on averages (compare sum × k' forms to avoid floats where you can).

LC 643 Max Average Subarray ILC 1456 Max Vowels in SubstringLC 1343 Subarrays Size K, Avg ≥ Threshold

2Fixed-Size Window with Frequency Map (anagram matching)

What it is: The window size is fixed by the pattern string's length, and the summary is a character-count map: does the current window hold exactly the same multiset of characters as the pattern? Slide across the text, updating two counts per step, and report every position where the counts match.

Find All Anagrams (LC 438) — s = "cbaebabacd", p = "abc" (window size 3)
c
0
b
1
a
2
e
3
b
4
a
5
b
6
a
7
c
8
d
9
"cba" (idx 0) and "bac" (idx 6) both have counts a:1 b:1 c:1 = pattern → matches at [0, 6]. Each slide touches only the two changed letters.

Intuition: an anagram is defined by character counts — order doesn't matter — so a window matches when its count map equals the pattern's. The efficiency trick: don't compare whole maps each step. Keep a running need counter (pattern characters still unmatched inside the window); the window is an anagram exactly when need == 0, and it moves by at most 1 per slide.

C++ · Find All Anagrams (LC 438) — the "need" counter
vector<int> findAnagrams(string s, string p) {
    vector<int> res;
    int n = s.size(), k = p.size();
    if (n < k) return res;
    int cnt[26] = {0};
    for (char c : p) ++cnt[c - 'a'];
    int need = k;                                    // chars still needed in the window
    for (int r = 0; r < n; ++r) {
        if (cnt[s[r]-'a']-- > 0) --need;             // consumed a needed char
        if (r >= k && cnt[s[r-k]-'a']++ >= 0) ++need; // char leaving gives one back
        if (need == 0) res.push_back(r - k + 1);
    }
    return res;
}

Challenges & pitfalls

Pitfall 5 (stale zeros) if you compare maps directly; the become/stop asymmetry of the counter (a count going from short-of to exactly-right decrements need; overshooting increments it back); when the alphabet is large (Unicode), map comparison stops being "constant" — say so.

LC 567 Permutation in StringLC 438 Find All AnagramsLC 187 Repeated DNA Sequences

3Longest Window Under a Constraint

What it is: The workhorse: find the longest contiguous run satisfying a rule — no repeated characters, at most k distinct elements, all-ones-after-flips. Grow the right end greedily; whenever the rule breaks, move the left end just far enough to fix it; the answer is the biggest valid window you ever saw.

Longest Substring Without Repeating (LC 3) — s = "abcabcbb"
window "abc" (best 3); then 'a' repeats at idx 3 → jump l past the old 'a'
a
l→
b
1
c
2
a
r
b
4
c
5
b
6
b
7
on the repeat, l = max(l, last['a']+1) = 1 → window becomes "bca", still length 3. best = 3. The max(l, …) guard stops l sliding backward on a stale character.

Intuition: greedy growth is safe thanks to monotonicity: if [l, r] is invalid, every window containing it is invalid — so the left end never backs up, and each element enters and leaves at most once (amortized O(n)). The summary carries the rule: a last-seen map/set for "no repeats," a distinct-count map for "at most k kinds."

C++ · Longest Substring Without Repeating (LC 3)
int lengthOfLongestSubstring(string s) {
    vector<int> last(128, -1);                   // char → last index seen
    int l = 0, best = 0;
    for (int r = 0; r < (int)s.size(); ++r) {
        if (last[s[r]] >= l) l = last[s[r]] + 1; // jump past prev copy (never backward)
        last[s[r]] = r;
        best = max(best, r - l + 1);
    }
    return best;
}

Challenges & pitfalls

The max(l, …) guard on the jump variant (without it l moves backward when an old character reappears behind the window — a classic silent bug); pitfall 5 on distinct counts; recognizing costumes — "fruit into baskets" is just "at most 2 distinct," and many problems bury the rule in a story.

LC 3 Longest Substring w/o RepeatingLC 904 Fruit Into BasketsLC 1695 Maximum Erasure Value
Example · Fruit Into Baskets (LC 904) — [1,2,1,2,3], at most 2 kinds
  • "Longest window with at most 2 distinct values." Window grows [1,2,1,2] (2 kinds, length 4). Adding 3 makes 3 kinds → shrink from the left until 2 remain.
  • Best = 4 (the run [1,2,1,2]).

4Budget Windows (longest with at most k "fixes")

What it is: A sub-species of Pattern 3: the window may contain up to k violations — k zeros you can flip, k characters you can replace, k deletions — and you want the longest such window. The summary tracks the cost to fix; shrink when the cost passes the budget k.

Max Consecutive Ones III (LC 1004) — flip at most k = 2 zeros
nums = [1,1,1,0,0,0,1,1,1,1,0] — best window (idx 5–10) holds exactly 2 zeros
1
1
1
0
0
0
1
1
1
1
0
reframe "flip ≤ 2 zeros" as "longest window with at most 2 zeros" → flip both → six 1s → answer 6.
windowzero spent from budget

Intuition: "flip at most k zeros for the longest run of 1s" = "longest window containing at most k zeros" — the budget is the constraint, and it's monotonic. For character replacement (424): a window is fixable when window_len − count_of_most_frequent_char ≤ k — everything except the majority character gets replaced.

C++ · Max Consecutive Ones III (LC 1004)
int longestOnes(vector<int>& nums, int k) {
    int l = 0, zeros = 0, best = 0;
    for (int r = 0; r < (int)nums.size(); ++r) {
        if (nums[r] == 0) ++zeros;
        while (zeros > k) if (nums[l++] == 0) --zeros;   // shrink until budget ok
        best = max(best, r - l + 1);
    }
    return best;
}
The non-shrinking optimization (longest-only): since only a bigger window improves the answer, never shrink below the best size — replace while (invalid) shrink with if (invalid) slide both ends by one. In 424 this makes max_freq go stale after slides, which is provably fine (a stale max only makes you more conservative; the true best was recorded when it was fresh). Be ready to defend that.

Challenges & pitfalls

The stale-max_freq argument (know it, or use the honest shrinking version); off-by-one between "at most k" and "exactly k"; delete-one variants (1493) where the window's answer is len − 1 even when it's all ones — read the statement's edge exactly.

LC 1004 Max Consecutive Ones IIILC 424 Longest Repeating Char ReplacementLC 1493 Longest Subarray of 1's After Deletion

5Shortest Window Satisfying a Constraint

What it is: The mirror of Pattern 3: find the smallest window that satisfies the rule — the shortest subarray with sum ≥ target, the shortest substring of s covering all of t. Grow until satisfied, then shrink aggressively while it stays satisfied, recording each valid size.

Minimum Size Subarray Sum (LC 209) — nums = [2,3,1,2,4,3], target 7
grow to sum ≥ 7, then shrink while STILL ≥ 7, recording lengths
2
3
1
2
4
l
3
r
windows recorded: [2,3,1,2]→4, [1,2,4]→3, [4,3]→2. Shortest = 2. Recording inside the shrink loop captures the smallest valid size.

Intuition: once the window satisfies the rule, growing further can't give a shorter answer — so the moment you're valid, squeeze from the left until you break validity, collecting candidate sizes. The record-inside-the-shrink-loop placement (Template S) is the exact mirror of Pattern 3, and the #1 thing people flip the wrong way.

C++ · Minimum Size Subarray Sum (LC 209)
int minSubArrayLen(int target, vector<int>& nums) {
    int l = 0, best = INT_MAX; long long sum = 0;
    for (int r = 0; r < (int)nums.size(); ++r) {
        sum += nums[r];
        while (sum >= target) {              // shrink while STILL valid
            best = min(best, r - l + 1);     // record before breaking
            sum -= nums[l++];
        }
    }
    return best == INT_MAX ? 0 : best;
}

Challenges & pitfalls

Template mirroring (shrink-while-valid vs shrink-while-invalid); 76 is the hardest mainstream window problem — required-vs-window counts, a formed counter, and recording (length, start) to rebuild the substring — write it fully twice before interviews; the empty-answer edge (no valid window → sentinel handling).

LC 209 Minimum Size Subarray SumLC 76 Minimum Window SubstringLC 1234 Replace the Substring for Balanced String
Example · Minimum Window Substring (LC 76) — s = "ADOBECODEBANC", t = "ABC"
  • Grow until the window contains an A, a B, and a C (track a formed counter). First valid: "ADOBEC"; shrink while still covering.
  • Smallest covering window found is "BANC" (length 4).

6Counting Subarrays (per-right-end + the atMost trick)

What it is: Not longest or shortest but how many subarrays satisfy the rule. Two tools: (a) for monotonic "≤"-style rules, every valid window [l, r] means all sub-windows ending at r are valid too → add r − l + 1 per step; (b) for "exactly k" rules, count atMost(k) − atMost(k−1) — two easy windows instead of one impossible one.

Subarray Product Less Than K (LC 713) — nums = [10,5,2,6], k = 100
r (val)productshrink?window+ (r−l+1)total
0 (10)10[10]+11
1 (5)50[10,5]+23
2 (2)100→10drop 10, l=1[5,2]+25
3 (6)60[5,2,6]+38
adding the window length each step counts all subarrays ending at r at once → 8.

Intuition: (a) works because validity is downward-closed: if [l, r] is valid, so are [l+1, r], …, [r, r] — the number of valid left-ends is just the window length. (b) works because "exactly k" isn't monotonic, but "at most k" is — and exactly-k = atMost(k) − atMost(k−1), turning one impossible problem into two Template C runs. This subtraction is the signature move; interviewers watch for it.

C++ · Subarray Product Less Than K (LC 713)
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
    if (k <= 1) return 0;
    int l = 0, prod = 1, count = 0;
    for (int r = 0; r < (int)nums.size(); ++r) {
        prod *= nums[r];
        while (prod >= k) prod /= nums[l++];   // restore validity
        count += r - l + 1;                    // subarrays ending at r
    }
    return count;
}
// exactly-k distinct (992):  atMost(k) - atMost(k-1)

Challenges & pitfalls

Where the count += r − l + 1 line goes (after restoring validity); convincing yourself of downward-closure for this rule before using (a); reaching for a window on 560-style "sum equals k with negatives" (that's prefix-sum + hashmap, not a window — knowing when to say "this isn't sliding window" scores too).

LC 713 Subarray Product Less Than KLC 930 Binary Subarrays With SumLC 992 Subarrays with K Different Integers
Example · exactly-k (LC 992) — nums = [1,2,1,2,3], k = 2 distinct
  • "Exactly 2" won't shrink cleanly, but "at most k distinct" is a normal Template C window.
  • atMost(2) = 12, atMost(1) = 5 → exactly-2 = 12 − 5 = 7. One hard problem became two easy ones.

7Monotonic-Deque Windows (max/min of a moving window)

What it is: The summary you need is the window's max or min — which a plain counter can't un-do (remove the max and you don't know the runner-up). Keep a deque of candidate indices in decreasing order (for max): the front is the current max, expired indices pop from the front, and smaller-and-newer-beaten values pop from the back.

Sliding Window Maximum (LC 239) — nums = [1,3,−1,−3,5,3,6,7], k = 3
deque holds indices whose values decrease; front = window max
windowdeque (values)max
[1, 3, −1][3, −1]3
[3, −1, −3][3, −1, −3]3
[−1, −3, 5][5]5
[−3, 5, 3][5, 3]5
[5, 3, 6][6]6
[3, 6, 7][7]7
answer [3, 3, 5, 5, 6, 7]. Each index enters and leaves the deque once → amortized O(n).

Intuition: when a new element enters, every smaller element still in the deque can never be a future maximum (the newcomer is bigger and outlives them) — so throw them away. What's left is a decreasing staircase of "values that could still matter." The deque is its own reusable micro-pattern (next-greater-element reuses it).

C++ · Sliding Window Maximum (LC 239)
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    deque<int> dq;                                    // indices, values decreasing
    vector<int> res;
    for (int r = 0; r < (int)nums.size(); ++r) {
        while (!dq.empty() && nums[dq.back()] <= nums[r]) dq.pop_back();
        dq.push_back(r);
        if (dq.front() <= r - k) dq.pop_front();      // drop expired front
        if (r >= k - 1) res.push_back(nums[dq.front()]);
    }
    return res;
}

Challenges & pitfalls

Storing values instead of indices (you lose expiry detection); <= vs < when popping the back (decides tie behavior — be consistent); 862 (shortest subarray sum ≥ k with negatives) is a different algorithm wearing deque clothes — prefix sums + a monotonic deque over prefix indices; explain the amortized O(n) when asked.

LC 239 Sliding Window MaximumLC 1438 Longest Subarray Abs Diff ≤ LimitLC 862 Shortest Subarray Sum ≥ K

8Rolling-Hash Windows (Rabin–Karp)

What it is: The window summary is a hash of the window's contents, updatable in O(1) as it slides (subtract the leaving character's contribution, multiply, add the entering one). Turns "compare every length-L window against a pattern / against each other" from O(nL) into O(n).

Rolling hash — treat the window as a base-b number mod a prime
ACGT → slide → ACGTA
hash = (hash − leaving·bL−1) · b + entering. Equal windows ⇒ equal hashes; a hash hit is a candidate you then verify by direct compare.

Intuition: treat the window as a number in base b (b > alphabet size), mod a big prime; sliding is one multiply-add. Equal windows ⇒ equal hashes; unequal hashes ⇒ definitely different — so hash first, verify contents only on a hit. The marquee composition: 1044 = binary search (on answer length L) + rolling hash (do any two length-L windows collide?) — a two-file combo worth naming.

C++ · Repeated DNA Sequences (LC 187) — length-10 windows
vector<string> findRepeatedDnaSequences(string s) {
    unordered_set<string> seen, repeated;
    for (int i = 0; i + 10 <= (int)s.size(); ++i) {
        string w = s.substr(i, 10);
        if (!seen.insert(w).second) repeated.insert(w);   // insert failed ⇒ already present
    }
    return {repeated.begin(), repeated.end()};
}
// Rabin–Karp: roll a base-b hash mod a large prime instead of substr(); watch overflow (use long long).

Challenges & pitfalls

Modular-arithmetic slips (negative intermediates — add p before % p); collision policy — decide verify-or-double-hash before coding; overflow in fixed-width C++ (use long long, or unsigned 64-bit and let it wrap); for tiny alphabets/lengths (187), a direct substring set is honestly fine — right-size the tool.

LC 187 Repeated DNA SequencesLC 28 Find First Occurrence (Rabin–Karp)LC 1044 Longest Duplicate Substring

Recognition Cheat Sheet

#PatternWindowTrigger phraseSummary maintained
1Fixed numericFixed k"every subarray of size k"running sum/count
2Fixed + freq mapFixed = pattern len"anagram/permutation of p in s"char counts + need/matched counter
3Longest under constraintVariable"longest substring/subarray with ⟨rule⟩"set / distinct-count map
4Budget windowVariable"at most k flips/replacements/deletions"violation count vs budget
5Shortest satisfyingVariable"minimum window/subarray such that"sum / formed counter
6CountingVariable"number of subarrays with ⟨rule⟩ / exactly k"count += r−l+1; atMost(k)−atMost(k−1)
7Monotonic dequeEither"max/min of each window", "abs diff ≤ limit"candidate deque (indices)
8Rolling hashFixed L"repeated/duplicate substring", string searchO(1)-slidable hash

Decision Flow, Study Order & The Interview Sentence

Decision flow: contiguous? → no: not a window. k given? → Template F (1–2). Optimizing what? longest → 3/4 · shortest → 5 · count → 6. Summary needs max/min? → 7. Content-identity across windows? → 8. Negatives + sum constraint? → window unsound: prefix sums (560/862).

Study order: 13 (two foundations) → 5 (mirror discipline; finish with 76) → 46 (the atMost trick) → 278
"The constraint is monotonic — expanding only ⟨worsens⟩ it, shrinking only ⟨repairs⟩ it — so a sliding window gives O(n): each pointer moves forward at most n steps. My window summary is ⟨X⟩, updated in O(1) per slide. And since the input is non-negative, the monotonicity actually holds."