12 Sliding Window LeetCode Problems That Cover Every Interview Variation
- Fixed-size windows use a sliding sum: add the entering element, subtract the leaving one, update any aggregate data structure.
- Variable-size windows expand
rightuntil a constraint breaks, then shrinkleftuntil the window is valid again. - Frequency maps with a
formedcounter replace O(alphabet) map comparisons with O(1) bookkeeping in problems like Permutation in String and Minimum Window Substring. - Non-shrinking windows (LC 424) only grow forward; the invariant is maximum valid size seen, not current window validity.
- Exactly-K constraints decompose into
atMost(K) - atMost(K-1), each solved independently with a standard variable window. - Sliding Window Maximum (LC 239) is a monotonic deque problem inside a sliding window frame, not a pure sliding window.
- Naming the template and invariant before coding is the signal that separates a passing interview from a no-hire.
You've got 150 sliding window problems in your queue. Your coffee is cold, your wrists hurt, and you just solved problem 47, which is functionally identical to problem 3, which you already forget how you solved. This is the grinding trap: volume without structure.
There are roughly 150 sliding window problems on LeetCode. Most are small remixes of six or seven distinct templates. Grind all 150 without identifying the templates and you will still freeze when a new problem looks 15% different.
These twelve, solved in this order, cover every variation. Each problem is here because it teaches something the previous one does not.
Two Sliding Window Templates, Not One
Sliding window comes in two fundamentally different forms. Confusing them is the most common source of bugs.
Fixed-size window: the window length is given. Add the rightmost element, remove the leftmost, maintain whatever aggregate you need.
Variable-size window: expand right until you violate a condition, then shrink left until you are valid again (or the reverse for minimization). The condition check lives at every right-pointer step, not just at the end.
Variable windows are closely related to the two-pointer technique, but the pointers here move in one direction only, never crossing. If you have not nailed the distinction, start with the pattern recognition guide first.

Tier 1: Fixed Window
1. Maximum Average Subarray I (LC 643)
The simplest fixed window. Find the subarray of length k with the maximum average. Every senior engineer has seen this. Most juniors get it wrong the first time because they reach for a nested loop.
def findMaxAverage(nums: list[int], k: int) -> float: window_sum = sum(nums[:k]) best = window_sum for i in range(k, len(nums)): window_sum += nums[i] - nums[i - k] best = max(best, window_sum) return best / k
This teaches the sliding sum: add the element entering, subtract the one leaving. Every fixed-window problem reduces to this mechanical update. Get it automatic before moving on.
2. Contains Duplicate II (LC 219)
Check if any two equal elements are within k index distance. The window is still fixed at size k, but the aggregate is a hash set, not a running sum. This one trips people up because "sliding window" stops feeling like "sliding window" once the aggregate stops being a number.
def containsNearbyDuplicate(nums: list[int], k: int) -> bool: window = set() for i, n in enumerate(nums): if n in window: return True window.add(n) if len(window) > k: window.remove(nums[i - k]) return False
The lesson: the aggregate inside a window can be any data structure, not just a number. When the window slides, clean up the structure by removing the element that left. More general than it looks.
Tier 2: Variable Window
3. Longest Substring Without Repeating Characters (LC 3)
The canonical variable-window problem. Expand right, shrink left whenever a duplicate enters the window. This is the one everyone has solved, often incorrectly the first three times.
def lengthOfLongestSubstring(s: str) -> int: seen = {} left = 0 best = 0 for right, ch in enumerate(s): if ch in seen and seen[ch] >= left: left = seen[ch] + 1 seen[ch] = right best = max(best, right - left + 1) return best
You shrink left not by looping, but by jumping it directly to the position after the last duplicate. The hash map stores last-seen index, not just presence. That jump is O(1) instead of O(n) and matters under interview time pressure.
4. Minimum Size Subarray Sum (LC 209)
First minimization problem. Find the shortest subarray with sum at least target. Now you shrink left aggressively, inside a while loop, to find the minimum valid window. This is where candidates start writing bugs.
def minSubArrayLen(target: int, nums: list[int]) -> int: left = 0 current_sum = 0 best = float('inf') for right in range(len(nums)): current_sum += nums[right] while current_sum >= target: best = min(best, right - left + 1) current_sum -= nums[left] left += 1 return 0 if best == float('inf') else best
The inner while loop is what separates minimization from maximization. In maximization you record the window when it becomes valid. In minimization you record it, then keep shrinking until it breaks, squeezing out every possible improvement.
5. Fruit Into Baskets (LC 904)
You have two baskets, each holding one fruit type. Find the maximum number of fruits you can collect from a row. Reframe: longest subarray with at most 2 distinct values. The problem statement is a riddle wrapped in produce.
def totalFruit(fruits: list[int]) -> int: count = {} left = 0 best = 0 for right, fruit in enumerate(fruits): count[fruit] = count.get(fruit, 0) + 1 while len(count) > 2: count[fruits[left]] -= 1 if count[fruits[left]] == 0: del count[fruits[left]] left += 1 best = max(best, right - left + 1) return best
This is the template for "at most K distinct elements" with a frequency map. Replace 2 with k and you have a general solution. Every constraint-counting sliding window problem follows this exact pattern.
Tier 3: Frequency Map Fixed Window
6. Permutation in String (LC 567)
Given strings s1 and s2, return true if any permutation of s1 is a substring of s2. Fixed window of size len(s1), compare character frequencies. The naive approach compares two frequency maps at every step and is O(26n). You do not want the O(26n) approach in an interview.
def checkInclusion(s1: str, s2: str) -> bool: if len(s1) > len(s2): return False need = {} for ch in s1: need[ch] = need.get(ch, 0) + 1 window = {} formed = 0 required = len(need) for right in range(len(s2)): ch = s2[right] window[ch] = window.get(ch, 0) + 1 if ch in need and window[ch] == need[ch]: formed += 1 if right >= len(s1): left_ch = s2[right - len(s1)] if left_ch in need and window[left_ch] == need[left_ch]: formed -= 1 window[left_ch] -= 1 if window[left_ch] == 0: del window[left_ch] if formed == required: return True return False
The formed counter avoids comparing two frequency maps at each step. Instead of an O(26) comparison every right-pointer advance, you track how many character types have hit their required count. When formed == required, the window is valid. This counter reappears verbatim in problem 11.
7. Find All Anagrams in a String (LC 438)
Identical setup to problem 6. Return all starting indices of anagram occurrences instead of just true/false.
If you understood problem 6, this is one line different. Solving both back to back teaches something the problems alone do not: the same template handles "does it exist" and "find all positions" with a trivial modification. That generalization is exactly what an interviewer expects you to name on the follow-up. "Oh, and if you wanted all positions instead of just the first, you'd just collect indices instead of returning early" is a sentence that sounds great in an interview room.
Tier 4: Non-Obvious Invariants
8. Longest Repeating Character Replacement (LC 424)
Find the longest substring where you can replace at most k characters to make all characters identical. The window is valid when window_size - max_frequency_in_window <= k. This one breaks people who try to intuit the invariant instead of deriving it.
def characterReplacement(s: str, k: int) -> int: count = {} left = 0 max_freq = 0 for right in range(len(s)): ch = s[right] count[ch] = count.get(ch, 0) + 1 max_freq = max(max_freq, count[ch]) if (right - left + 1) - max_freq > k: count[s[left]] -= 1 left += 1 return len(s) - left
The window never shrinks below its maximum valid size, so len(s) - left gives the answer directly. Once the window grows to a new maximum, it only slides forward. You only care about windows larger than anything you have already found, so shrinking is wasted work. Counterintuitive until you see it once, obvious forever after.
9. Max Consecutive Ones III (LC 1004)
Binary array. Flip at most k zeros. Find the longest subarray of ones after flipping.
The invariant: the window is valid while zeros_in_window <= k.
def longestOnes(nums: list[int], k: int) -> int: left = 0 zeros = 0 best = 0 for right in range(len(nums)): if nums[right] == 0: zeros += 1 while zeros > k: if nums[left] == 0: zeros -= 1 left += 1 best = max(best, right - left + 1) return best
This is the same structure as problem 8 but with a transparent invariant. After LC 424, this feels easy. That contrast is intentional: seeing the hard case first makes the obvious case automatic.
Tier 5: Advanced Patterns
10. Subarrays with K Different Integers (LC 992)
Count subarrays with exactly k distinct integers. The "exactly K" constraint breaks the standard shrink-until-valid approach because shrinking past the boundary overshoots. First time you hit this in an interview, it feels like the problem is broken.
The trick: exactly(K) = atMost(K) - atMost(K-1). Each atMost call is a standard frequency-map variable window. Count valid subarrays by adding right - left + 1 at each step, since each new right pointer creates exactly that many new subarrays ending at it.
def subarraysWithKDistinct(nums: list[int], k: int) -> int: def at_most(k: int) -> int: count = {} left = 0 result = 0 for right in range(len(nums)): count[nums[right]] = count.get(nums[right], 0) + 1 while len(count) > k: count[nums[left]] -= 1 if count[nums[left]] == 0: del count[nums[left]] left += 1 result += right - left + 1 return result return at_most(k) - at_most(k - 1)
Once you internalize this, any "exactly K" constraint with a monotonic "at most K" equivalent collapses into two sliding window calls.
11. Minimum Window Substring (LC 76)
Find the smallest window in s that contains all characters of t. Variable window: expand until valid, record the window, then shrink from the left to find a smaller one. The formed counter from problem 6 reappears. If you skipped problem 6, you will rewrite it here under pressure.
def minWindow(s: str, t: str) -> str: if not t or not s: return "" need = {} for ch in t: need[ch] = need.get(ch, 0) + 1 required = len(need) window = {} formed = 0 left = 0 best = float('inf'), 0, 0 for right in range(len(s)): ch = s[right] window[ch] = window.get(ch, 0) + 1 if ch in need and window[ch] == need[ch]: formed += 1 while formed == required: if right - left + 1 < best[0]: best = right - left + 1, left, right left_ch = s[left] window[left_ch] -= 1 if left_ch in need and window[left_ch] < need[left_ch]: formed -= 1 left += 1 return "" if best[0] == float('inf') else s[best[1] : best[2] + 1]
This problem synthesizes everything: variable window, frequency map, formed counter, and aggressive shrinking until the window breaks. Code it cold in 20 minutes and you have mastered the category. It is not last because it is unusual. It is last because nothing after it adds anything new, except one thing.
12. Sliding Window Maximum (LC 239)
Find the maximum in every window of size k as it slides across the array. Naive: O(nk). Required: O(n), via a monotonic deque.
from collections import deque def maxSlidingWindow(nums: list[int], k: int) -> list[int]: dq = deque() # stores indices; front always holds the index of the current max result = [] for i, n in enumerate(nums): while dq and nums[dq[-1]] <= n: dq.pop() dq.append(i) if dq[0] < i - k + 1: dq.popleft() if i >= k - 1: result.append(nums[dq[0]]) return result
This is not a sliding window problem dressed as one. It is a monotonic deque problem that happens to use a sliding window frame. Elements are added at the back, expired at the front, and anything in the back smaller than a new arrival is discarded because it can never be a future maximum. That is a fundamentally different data structure from everything above. Recognizing the distinction is half the battle. Naming it out loud in an interview is the other half.
The Part That Actually Gets You Hired
These twelve problems cover: sliding sum, sliding set, shrink-on-violation, aggressive minimization, frequency map matching, non-shrinking windows, exactly-K via subtraction, and monotonic deque. Interview sliding window problems are built from these pieces, combinatorially.
The part that trips engineers is not the knowledge. It is articulating which template applies and why, under pressure, while talking. The sliding window algorithm post covers the core mechanics if you need a refresher on why this pattern eliminates the nested loop at all. Naming your template choice before you code, and explaining the invariant while you type, is what separates a candidate who "knows" sliding window from one who demonstrates it to an interviewer. SpaceComplexity runs timed voice-based mock interviews with rubric feedback on your explanation, not just your code. If you want to pressure-test whether you can narrate template selection in real time, that is the right tool for it.
What to Remember
- Fixed window: add one element, remove one, update the aggregate.
- Variable window: expand right, shrink left until the invariant holds.
- Frequency maps need a
formedcounter to avoid O(alphabet) comparison per step. - Non-shrinking windows (LC 424) track the maximum valid size, not the current size.
- Exactly K = atMost(K) - atMost(K-1) for any monotonic counting constraint.
- Sliding window maximum requires a monotonic deque. Not the same structure.