LC Patterns — Sliding Window
◆ 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?
§0The Four Templates & Universal Pitfalls
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
}
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.
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).
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.
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.
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.
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.
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."
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.
- "Longest window with at most 2 distinct values." Window grows
[1,2,1,2](2 kinds, length 4). Adding3makes 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.
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.
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;
}
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.
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.
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).
- Grow until the window contains an
A, aB, and aC(track aformedcounter). 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.
| r (val) | product | shrink? | window | + (r−l+1) | total |
|---|---|---|---|---|---|
| 0 (10) | 10 | — | [10] | +1 | 1 |
| 1 (5) | 50 | — | [10,5] | +2 | 3 |
| 2 (2) | 100→10 | drop 10, l=1 | [5,2] | +2 | 5 |
| 3 (6) | 60 | — | [5,2,6] | +3 | 8 |
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.
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).
- "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.
| window | deque (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 |
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.
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).
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 windowsvector<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.
▤Recognition Cheat Sheet
| # | Pattern | Window | Trigger phrase | Summary maintained |
|---|---|---|---|---|
| 1 | Fixed numeric | Fixed k | "every subarray of size k" | running sum/count |
| 2 | Fixed + freq map | Fixed = pattern len | "anagram/permutation of p in s" | char counts + need/matched counter |
| 3 | Longest under constraint | Variable | "longest substring/subarray with ⟨rule⟩" | set / distinct-count map |
| 4 | Budget window | Variable | "at most k flips/replacements/deletions" | violation count vs budget |
| 5 | Shortest satisfying | Variable | "minimum window/subarray such that" | sum / formed counter |
| 6 | Counting | Variable | "number of subarrays with ⟨rule⟩ / exactly k" | count += r−l+1; atMost(k)−atMost(k−1) |
| 7 | Monotonic deque | Either | "max/min of each window", "abs diff ≤ limit" | candidate deque (indices) |
| 8 | Rolling hash | Fixed L | "repeated/duplicate substring", string search | O(1)-slidable hash |
↳Decision Flow, Study Order & The Interview Sentence
Study order: 1 → 3 (two foundations) → 5 (mirror discipline; finish with 76) → 4 → 6 (the atMost trick) → 2 → 7 → 8