Sliding Window: Practice Problems with Solutions
Welcome to the practice problems for the sliding window pattern. If you need a refresher on the code, the code templates page has the fixed-size and variable-size patterns in 6 languages. Each problem below includes the reason the pattern applies, a hint that stops short of the answer, and the full solution.
Recommended Study Order
The problems are ordered by difficulty and by the size of the conceptual jump between them.
- Maximum Average Subarray I teaches the fixed-size window with a plain running sum. No other mechanics to juggle.
- Max Consecutive Ones III introduces the left pointer and the shrink loop with a simple validity rule. At most k zeros are allowed inside the window.
- Minimum Size Subarray Sum is the variable-size core: record the window, then shrink it. This is the most common interview variation.
- Permutation in String replaces the sum with a frequency comparison, which turns the window state into character counts.
- Longest Substring Without Repeating Characters adds the at-most-one constraint on character counts, the most common way to keep a window valid.
- Minimum Window Substring is the Hard capstone. It combines two frequency maps and a formed counter, and if you can write it from memory you know the pattern deeply.
Easy Problems
1. Maximum Average Subarray I
LeetCode 643 | Difficulty: Easy
Brief: Find the contiguous subarray of size k with the maximum average value.
Why this pattern: The window has a fixed size of exactly k elements. Once the first window is built, each slide adds one element on the right and removes one on the left, and the running sum updates in
Key Insight: The divisor never changes, so track the maximum sum and divide once at the end.
Visual:
graph TD
I["nums = 1 12 -5 -6 50 3, k = 4"] --> W1["window 1: 1+12-5-6 = 2"]
W1 --> W2["slide: 2 - 1 + 50 = 51"]
W2 --> W3["slide: 51 - 12 + 3 = 42"]
W3 --> M["max sum 51, average = 51 / 4 = 12.75"]
style M fill:#d8f5d8,stroke:#333
Code:
var findMaxAverage = function(nums, k) {
let sum = 0;
for (let i = 0; i < k; i++) {
sum += nums[i];
}
let maxSum = sum;
for (let i = k; i < nums.length; i++) {
// Remove the element leaving the window, add the new one
sum = sum - nums[i - k] + nums[i];
maxSum = Math.max(maxSum, sum);
}
return maxSum / k;
};The fixed window never needs a while loop because the window size is a constant of the problem. The only bookkeeping is subtracting the element that just left and adding the element that just entered.
Medium Problems
2. Max Consecutive Ones III
LeetCode 1004 | Difficulty: Medium
Brief: Find the longest subarray that contains at most k zeros, so that flipping those zeros produces all ones.
Why this pattern: The window is variable and the validity rule is a count. The number of zeros inside the window must stay at or below k, which gives the shrink condition its simple shape.
Key Insight: Only the zeros matter. When the window holds k+1 zeros, shrink from the left until a zero leaves the window, restoring validity.
Visual:
graph LR
A["nums = 1 0 1 0 1, k = 1"] --> B["right = 3: zeros = 2 > 1"]
B --> C["shrink: drop one zero, zeros = 1"]
C --> D["window 1 0 1, length 3"]
D --> E["answer 3"]
style E fill:#d8f5d8,stroke:#333
Code:
function longestOnes(nums, k) {
let left = 0;
let zeros = 0;
let maxLen = 0;
for (let right = 0; right < nums.length; right++) {
if (nums[right] === 0) zeros++;
// A window with k+1 zeros is invalid. Shrink from the
// left until a zero has left the window
while (zeros > k) {
if (nums[left] === 0) zeros--;
left++;
}
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}The total work stays linear because left passes each position at most once. When the window is invalid, no re-scanning happens, the left edge just walks forward until a zero leaves, and those positions can never be part of a valid window again.
3. Minimum Size Subarray Sum
LeetCode 209 | Difficulty: Medium
Brief: Find the minimal length of a contiguous subarray whose sum is at least the target.
Why this pattern: Adding elements to the window can only raise the sum, and removing them can only lower it. That monotonic property is exactly what a shrink loop needs, so this problem is the canonical one for the pattern.
Key Insight: Record the window length before every shrink. Once the sum meets the target, every smaller window you reach by dropping elements is also a candidate.
Visual:
graph TD
A["right=3: 2 3 1 2, sum 8, len 4"] --> B["shrink: 3 1 2, sum 6"]
B --> C["right=4: 3 1 2 4, sum 10, len 4"]
C --> D["shrink: 1 2 4, sum 7, len 3"]
D --> E["right=5: 2 4 3, sum 9"]
E --> F["shrink: 4 3, sum 7, len 2"]
F --> G["answer 2"]
style G fill:#d8f5d8,stroke:#333
Code:
var minSubArrayLen = function(target, nums) {
let left = 0;
let sum = 0;
let minLen = Infinity;
for (let right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) {
// Record before shrinking: the next window may be
// shorter, but this one is still valid right now
minLen = Math.min(minLen, right - left + 1);
sum -= nums[left];
left++;
}
}
return minLen === Infinity ? 0 : minLen;
};The one detail to keep straight is that the answer is recorded before the shrink, never after. This is the number one place learners drop the smallest window. The solution assumes the array has no negatives, because the sum must be monotonic for the shrink to be safe. With negatives in the input, switch to a prefix-sum or Kadane-style approach instead.
4. Permutation in String
LeetCode 567 | Difficulty: Medium
Brief: Return true if s2 contains a substring that is a permutation of s1.
Why this pattern: A permutation of s1 is exactly any window of length len(s1) with the same character counts. Fixed window size plus a frequency state is all the problem turns into.
Key Insight: Keep two 26-element tables, one for s1 and one for the window. Slide the window and compare tables. A match at any position means the permutation exists.
Visual:
graph LR
A["s1 = ab, s2 = eidbaooo"] --> B["window: ei"]
B --> C["id"]
C --> D["db"]
D --> E["ba: counts match"]
style E fill:#d8f5d8,stroke:#333
Code:
function checkInclusion(s1, s2) {
if (s1.length > s2.length) return false;
const target = new Array(26).fill(0);
const window = new Array(26).fill(0);
for (let i = 0; i < s1.length; i++) {
target[s1.charCodeAt(i) - 97]++;
window[s2.charCodeAt(i) - 97]++;
}
for (let i = s1.length; i < s2.length; i++) {
if (window.every((count, j) => count === target[j])) return true;
// Slide: add the entering character, remove the leaving one
window[s2.charCodeAt(i) - 97]++;
window[s2.charCodeAt(i - s1.length) - 97]--;
}
return window.every((count, j) => count === target[j]);
}Because the alphabet is small and fixed, an integer array of size 26 beats a hash map in both time and space. This is the same idea as the frequency map template, with the window size pinned so no shrink is ever needed.
5. Longest Substring Without Repeating Characters
LeetCode 3 | Difficulty: Medium
Brief: Find the length of the longest substring without repeating characters.
Why this pattern: The window is variable and the validity rule targets character counts. When a character repeats, the window is invalid and the left edge must move past the earlier copy.
Key Insight: Instead of shrinking one character at a time, the map stores the last index of each character, so the left edge jumps straight past the repeated character.
Visual:
graph LR
A["abcabcbb"] --> B["right=3: 'a' seen at 0, left -> 1"]
B --> C["window bca, length 3"]
C --> D["later: 'b' seen, left -> 5"]
D --> E["answer 3"]
style E fill:#d8f5d8,stroke:#333
Code:
var lengthOfLongestSubstring = function(s) {
let left = 0;
let maxLen = 0;
const lastSeen = new Map();
for (let right = 0; right < s.length; right++) {
// A repeated character moves the left edge past its
// previous occurrence, without rescanning the window
if (lastSeen.has(s[right])) {
left = Math.max(left, lastSeen.get(s[right]) + 1);
}
lastSeen.set(s[right], right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
};The map jumps the left edge straight past the previous occurrence of a character, which is faster than the shrink-one-by-one version from the template. The code stays linear, so this is the version you should aim to write from memory.
Hard Problems
6. Minimum Window Substring
LeetCode 76 | Difficulty: Hard
Brief: Find the minimum substring of s that contains every character of t, including duplicate counts.
Hint: Two maps, one for the target and one for the window, plus a formed counter that says how many target characters the window already satisfies in the right quantities. The window is valid the moment formed reaches the size of the target map.
Why this pattern: This is the hardest sliding window problem. The window must contain all characters of t with correct counts and stay minimal. It exercises the full variable-size window with an external frequency state.
Key Insight: Track a formed counter, the count of characters in t that the window currently satisfies. When formed equals the number of distinct characters in t, the window is valid and shrinking begins.
Visual:
graph LR
S["s = ADOBECODEBANC, t = ABC"] --> W1["first complete: ADOBEC, len 6"]
W1 --> W2["shrink + expand: BECODEBA, len 8"]
W2 --> W3["final: BANC, len 4"]
style W3 fill:#d8f5d8,stroke:#333
Code:
var minWindow = function(s, t) {
if (t.length > s.length) return "";
const target = new Map();
for (let char of t) {
target.set(char, (target.get(char) || 0) + 1);
}
const window = new Map();
let left = 0, formed = 0, required = target.size;
let minLen = Infinity, start = 0;
for (let right = 0; right < s.length; right++) {
window.set(s[right], (window.get(s[right]) || 0) + 1);
// Entering a character that reaches its required count
// marks the window as more complete
if (target.has(s[right]) && window.get(s[right]) === target.get(s[right])) {
formed++;
}
while (left <= right && formed === required) {
if (right - left + 1 < minLen) {
minLen = right - left + 1;
start = left;
}
// Removing a character that drops below its required
// count makes the window incomplete again
const leftChar = s[left];
window.set(leftChar, window.get(leftChar) - 1);
if (target.has(leftChar) && window.get(leftChar) < target.get(leftChar)) {
formed--;
}
left++;
}
}
return minLen === Infinity ? "" : s.substring(start, start + minLen);
};This problem is the whole pattern in one. It has a map for the target, a map for the window, a counter that says when the window is valid, and a shrink loop that runs only while the window stays valid. Compare the window map here with the frequency map template and you can see how the state grows into the counter logic.
Once Minimum Window Substring is memorized, the sliding window pattern is fully covered. From fixed-size running sums to two-map counters, the mechanics stay the same: expand right, shrink left, and record only valid windows. Nothing else changes regardless of the problem’s exact wording.