Patterns/Sliding Window

Sliding Window

Top interview pattern
70 min read
Updated June 2026
What you'll learn
  • State the three properties that make a problem solvable by sliding window: contiguity, incrementally maintainable state, and predictable validity
  • Classify any contiguous-subarray problem into one of four template families and explain why each template places the update where it does
  • Reconstruct the expand/shrink/update loop from first principles and explain why it runs in O(n)
  • Recognize when sliding window fails and name the correct alternative technique

Sliding window maintains a running state over a contiguous range and updates it incrementally as two boundaries move through the data, reducing O(n²) brute force to O(n). This guide builds the technique from three core properties, walks through a canonical problem, then derives four template families from the same skeleton. Each section ends with a reconstruction check so you can verify your understanding before moving on.

The Sliding Window Invariant

Sliding window applies when a problem has three properties. If any one fails, the technique breaks. Understanding why each property is necessary matters more than memorizing templates.

Vocabulary you need first

This guide uses a few terms repeatedly. Here is what each one means concretely:

"valid window" A window [left, right] that satisfies the problem's constraint right now. Example: if the constraint is "at most 2 distinct characters," then [a, b, a] is valid and [a, b, c] is not.

"invalid window" A window that violates the constraint. You must shrink it before recording an answer.

"at most k" The tracked quantity must be ≤ k. Example: "at most 2 distinct characters" means the window is valid when distinct count ≤ 2. Growing the window can push you over k (invalid). Shrinking can bring you back under (valid).

"at least target" The tracked quantity must be ≥ target. Example: "sum ≥ 7" means the window is valid when sum ≥ 7. Growing the window can make you reach the target (valid). Shrinking can drop you below (invalid).

"exactly k" The tracked quantity must equal k exactly. This usually cannot be solved with a single sliding window pass. See Section 6.

Property 1: The Answer Is a Contiguous Range

The answer is a contiguous range [left, right]. Every element between the two boundaries is part of the window. If you could skip elements, sliding window cannot enforce which elements are included, and you need a different technique (two pointers on sorted data, DP for subsequences, or hash map for arbitrary pairs).

Window: [2, 1, 5]
201152133425LR

Elements at indices 0, 1, 2

Property 2: O(1) State Updates

Adding or removing one element from the window boundary updates the tracked state in O(1). This is what makes the technique O(n) instead of O(n²). If recomputing state after each boundary move costs O(k) for window size k, you are back to brute force.

This table summarizes what state to track for common constraint types. If a row is not obvious, read the corresponding template in Section 4 first, then come back.

ConstraintWhat to trackData structureWhy O(1)
Sum constraints (nonneg)Running sumintAdding/removing one element is one arithmetic op
No duplicatesElements in windowsetO(1) add, remove, membership test
At most k distinctCount of each elementdict / CounterEach add/remove is O(1). len(dict) equals distinct count only if you delete keys when their count hits zero
Contains all chars of tChar counts + formed countdict + intformed tracks how many chars meet their quota
At most k zerosZero countintIncrement on 0 entry, decrement on 0 exit

Property 3: Growing the Window Can Only Hurt, Shrinking Can Only Help

Ask yourself: "When I add one more element on the right, does the tracked quantity move in one predictable direction?" For "at most 2 distinct characters," adding a character can only increase the distinct count (or keep it the same). It can never decrease it. That means growing the window can only push you toward invalidity, never toward validity. Conversely, removing an element from the left can only decrease the distinct count, moving you back toward validity.

This one-directional behavior is what makes the shrink loop work. The loop can always restore validity by shrinking, and it never needs to re-expand. Some resources call this property "monotonicity." The label does not matter. What matters: if growing can both help and hurt (as with negative numbers in a sum), the shrink loop cannot guarantee progress, and sliding window breaks.

When this property fails (e.g., negative numbers in a sum constraint), shrinking can increase the tracked quantity instead of decreasing it. The shrink loop no longer moves toward validity, and the algorithm skips valid windows or loops indefinitely.

The Generic Skeleton

Every sliding window solution fills the same three-step loop. The three properties above are what make this loop correct: the range must be contiguous, each step updates state in O(1), and shrinking always moves toward validity.

1left = 0
2answer = 0
3
4for right in range(len(arr)):
5 # 1. EXPAND: add arr[right] to the window state
6
7 # 2. SHRINK: move left forward while the window violates the constraint
8 while window_violates_constraint():
9 # remove arr[left] from the window state
10 left += 1
11
12 # Window [left, right] is now valid.
13
14 # 3. UPDATE: use the current window to improve the answer
15 # answer = max(answer, right - left + 1) # longest
16 # answer += right - left + 1 # counting

Note: For shortest problems, the update happens inside the shrink loop. See Template 3 in Section 4.

Spotting the Three Properties in Problem Statements

"contiguous subarray", "substring", "consecutive", "window"

Signals Property 1. The answer is a continuous slice [left, right].

"at most k", "at least k", "exactly k", "sum target"

Signals Property 3. Ask: does adding one more element make the constraint harder to satisfy? For "at most k distinct," adding an element can only increase distinct count, never decrease it. That one-directional pressure is what you need. If the constraint can get easier or harder unpredictably when growing, sliding window will not work.

"longest", "shortest", "count", "of size k"

Tells you which template to use. "Longest" means shrink while invalid, update after (Template 2). "Shortest" means shrink while valid, update inside (Template 3). "Count" uses the same structure as longest but accumulates right - left + 1 (Template 4). "Of size k" means fixed window, no shrink decision (Template 1).

"nonnegative integers", "positive numbers"

Confirms Property 3 for sum-based constraints. If negatives are present, sliding window on sums fails.

The Pattern in Action

Fixed-Window Warm-Up

Problem: Given [2, 1, 5, 1, 3, 2], find the maximum sum of any 3 consecutive elements. The window size is given, so there's no shrink decision. Just slide and update.

Fixed Window (k=3)

Window
[2, 1, 5]
Sum
8
Max
8
2[0]1[1]5[2]1[3]3[4]2[5]LR
Sliding
3
Naive
3

Step 1 of 8 · Sum the first k=3 elements: 2 + 1 + 5 = 8. This initial sum is the foundation. Every future slide updates it instead of recomputing from scratch.

When the window size is not given, the algorithm must decide when to shrink. That decision is what distinguishes the four template families.

Brute Force: Why O(n²) Is Wasteful

The naive approach checks every possible subarray:

1# Brute force: O(n^2) subarrays, each taking O(1) to O(n) to evaluate
2for left in range(n):
3 for right in range(left, n):
4 evaluate(arr[left:right+1]) # recomputes from scratch

Moving from subarray [1,2,3] to [1,2,3,4] reprocesses three elements that were already in the previous window. Only one element changed, but brute force recomputes the entire range.

Worked Example: Longest Substring Without Repeating Characters

Problem: Given "abcabcbb", find the length of the longest substring without repeating characters.

Variable Window (Expand & Shrink)

Window
"a"
Length
1
Best
1
Set
{a}
Status
EXPAND
a0b1c2a3b4c5b6b7LR

Step 1 of 15 · Invariant: the set tracks every unique char in [L..R]. "a" is new, so add it and expand.

1def longest_substring_no_repeat(s):
2 count = {}
3 left = 0
4 best = 0
5
6 for right in range(len(s)):
7 # EXPAND: add s[right]
8 count[s[right]] = count.get(s[right], 0) + 1
9
10 # SHRINK: while the new char is duplicated
11 while count[s[right]] > 1:
12 count[s[left]] -= 1
13 if count[s[left]] == 0:
14 del count[s[left]]
15 left += 1
16
17 # UPDATE: window is now valid
18 best = max(best, right - left + 1)
19
20 return best # returns 3 for "abcabcbb"

Why This Is O(n), Not O(n²)

The while loop inside the for loop looks like it could be O(n) per iteration, giving O(n²) total. It is not. Each index enters the window exactly once (when right advances) and leaves at most once (when left advances past it). Total pointer moves across the entire run:

+Each element enters oncen operations
Each element leaves oncen operations
=Total2n = O(n)
Key Insight
The Amortized O(n) Argument
Sliding window is O(n) because each index enters and leaves the window at most once, bounding total pointer movements to 2n. The while loop doesn't restart for each right. It picks up where left stopped. Space is O(1) for a fixed alphabet, or O(min(n, alphabet_size)) with a frequency map.

When the Invariant Breaks

Property 3 is the one that breaks most often. The clearest example: negative numbers in a sum constraint.

Failure Case: Negative Numbers

Consider [1, -1, 2, 3] with target sum 4. The shrink loop assumes that removing the leftmost element decreases the sum, moving toward validity. But removing the -1 at index 1 increases the sum from 4 to 5. The algorithm moves left past valid windows without recording them.

1-123sum = 5 > 3shrink left1-123sum = 4 > 3shrink left1-123sum = 5increased!?

Shrinking the window should never increase the sum. Negative numbers break that guarantee.

Here's why: shrinking removes a negative, which increases the sum instead of decreasing it. Property 3 requires that shrinking always moves the tracked quantity toward the "not violating" side. With negatives, shrinking can move it in either direction, so the algorithm can't make progress.

Alternative: Prefix sums + hash map. Convert each subarray sum to currentPrefix - earlierPrefix, and use a hash map to count how many earlier prefixes produce the target difference. This avoids the monotonicity requirement entirely.

Other Failure Modes

Non-contiguous elements: Property 1 fails. If elements don't need to be adjacent (e.g., "longest increasing subsequence"), use DP or binary search.

Expensive state updates: Property 2 fails. If recomputing state costs O(k) per step (e.g., "is this window a palindrome?"), you lose the O(n) advantage and are back to brute-force territory.

The Four Template Families

Every sliding window solution fills the same skeleton. What changes between templates: the goal, the shrink condition, and where the update goes.

Template 1: Fixed Size Window

The window size k is given. No shrink decision needed.

1def max_sum_of_k(nums, k):
2 n = len(nums)
3 if n < k:
4 return 0
5
6 # Build the first window of size k
7 window_sum = sum(nums[:k])
8 best = window_sum
9
10 # Slide one position at a time
11 for right in range(k, n):
12 out = right - k # index of element leaving
13 window_sum -= nums[out] # remove departing element
14 window_sum += nums[right] # add arriving element
15 best = max(best, window_sum)
16
17 return best

No while loop because the window size never changes. The outgoing index is always right - k. A running sum suffices because each step changes exactly two elements.

Template 2: Variable Window (Find Longest)

Maximize window length under an "at most" constraint. Example: longest substring with at most k distinct characters.

1def longest_k_distinct(s, k):
2 count = {}
3 left = 0
4 best = 0
5
6 for right in range(len(s)):
7 # 1. EXPAND
8 count[s[right]] = count.get(s[right], 0) + 1
9
10 # 2. SHRINK while invalid (too many distinct chars)
11 while len(count) > k:
12 count[s[left]] -= 1
13 if count[s[left]] == 0:
14 del count[s[left]]
15 left += 1
16
17 # 3. UPDATE: window is valid, record length
18 best = max(best, right - left + 1)
19
20 return best

The shrink loop uses while not if because a single violation may require removing multiple elements before validity is restored. The update goes after the shrink loop because the window must be valid before measuring its length.

Why frequency map, not set: A set tells you what is present but not how many of each. When a character leaves the window, you need the count to know whether it is still present elsewhere in the window. Only when the count drops to zero do you delete the key and reduce the distinct count.

Template 3: Variable Window (Find Shortest)

Minimize window length under an "at least" constraint. Example: minimum size subarray with sum >= target.

1def min_subarray_sum(nums, target):
2 left = 0
3 window_sum = 0
4 best = float('inf')
5
6 for right in range(len(nums)):
7 # 1. EXPAND
8 window_sum += nums[right]
9
10 # 2. SHRINK + UPDATE: while valid, record then shrink
11 while window_sum >= target:
12 best = min(best, right - left + 1) # update INSIDE
13 window_sum -= nums[left]
14 left += 1
15
16 return best if best != float('inf') else 0

The shrink condition is while valid, not while invalid. Every valid window is a candidate, and shorter valid windows are better, so the algorithm keeps shrinking as long as validity holds. The update goes inside the loop because each iteration starts with a valid window that might be the answer. The condition uses >= not > because a window with sum exactly equal to target is still valid.

See it in action: Minimum Window Substring

Given string s and target t, find the smallest window in s containing all characters of t. Expand until valid, then shrink while still valid, recording the candidate at each shrink step:

Minimum Window Substring · target = "ABC"

Window
"A"
Length
1
Formed
1/3
Min
Best
A0D1O2B3E4C5O6D7E8B9A10N11C12LR

Build two hash maps: "need" tracks required counts for A, B, C and "have" tracks current counts. The "formed" counter increments when a character meets its required count.

Template 4: Counting Valid Subarrays

Count how many subarrays satisfy an "at most" constraint. Example: count subarrays with at most k distinct elements.

1def count_at_most_k_distinct(nums, k):
2 count_map = {}
3 left = 0
4 result = 0
5
6 for right in range(len(nums)):
7 # 1. EXPAND
8 count_map[nums[right]] = count_map.get(nums[right], 0) + 1
9
10 # 2. SHRINK while invalid
11 while len(count_map) > k:
12 count_map[nums[left]] -= 1
13 if count_map[nums[left]] == 0:
14 del count_map[nums[left]]
15 left += 1
16
17 # 3. COUNT all valid subarrays ending at right
18 result += right - left + 1
19
20 return result

After shrinking, [left, right] is the longest valid window ending at right. Every subarray [i, right] where left ≤ i ≤ right is also valid, because removing elements from the left cannot violate an "at most" constraint. The count can only stay the same or go down. That yields exactly right - left + 1 valid subarrays ending at right.

This reasoning applies only to "at most" constraints. For "exactly k", shorter windows can drop below k, so the counting formula does not apply directly.

012345[2..5][3..5][4..5][5..5]4 starting positions5 - 2 + 1 = 4 subarrays

Longest vs Shortest

Templates 2 and 3 share the same skeleton but differ in two critical places: the shrink condition and the update placement. Swapping either one produces code that silently returns wrong answers.

Find LongestFind Shortest
Shrink conditionwhile invalid. Shrink only until the constraint is satisfied againwhile valid. Keep shrinking as long as the window still meets the constraint
Update placementAfter shrink loopInside shrink loop
Initializationbest = 0best = float('inf')
Goal of shrinkingRestore validity. You want the longest window, so shrink only until valid again. Any further would make it shorter than necessarySearch for the minimum. You want the shortest valid window, so keep shrinking while still valid. Each shrink is a potentially better answer

Code Comparison

Find Longest
1best = 0
2for right in range(n):
3 expand(right)
4 while invalid():
5 shrink(left)
6 left += 1
7 # valid here
8 best = max(best, right-left+1)

The shrink condition is while invalid(). The window shrinks only until it becomes valid again. The update (best = max(...)) happens after the shrink loop, when the window is guaranteed valid.

Common Mistake
Swapped shrink condition
If your shortest answer is unreasonably large (or inf), check whether you wrote while invalid instead of while valid. Shrinking while invalid in a shortest problem means you only shrink past the answer, never recording it.
Interview Tip
Name the variant before coding
Say out loud: "This is a find-shortest problem, so I shrink while valid and update inside the loop." This locks in the correct behavior and prevents the swap under time pressure.

Counting vs Optimizing

The counting template is structurally identical to Find Longest with one change: replace best = max(best, ...) with result += right - left + 1. Both shrink while invalid, both update after the shrink loop. The only difference is accumulation instead of maximization.

Counting Subarrays · k = 2 distinct

Window
[1]
Distinct
1
Valid
Added
+1
Total
1
1[0]2[1]1[2]2[3]3[4]LR+1 subarrays

Window [1] has 1 distinct, which is <= k=2. The key counting insight: every valid window [L..R] contributes (R-L+1) subarrays, one for each possible start point from L to R. Here that is 1.

The "Exactly K" Problem

Counting subarrays with exactly k distinct elements cannot use the counting template directly. The counting template relies on a key property: if a window [left, right] is valid under an "at most k" constraint, then every shorter window [i, right] where left <= i <= right is also valid, because removing elements can only decrease the distinct count. For "exactly k," that property fails. Consider the array [1, 2, 1] with k = 2. The window [0, 2] has 2 distinct elements (valid). But the sub-window [2, 2] = [1] has only 1 distinct element (invalid). Shorter windows can drop below k, so the right - left + 1 formula counts windows that are not all valid.

The solution splits "exactly k" into two "at most" passes:

1exactly(k) = atMost(k) - atMost(k - 1)

Here is why this works. atMost(k) counts every subarray with 0, 1, 2, ..., or k distinct elements. atMost(k-1) counts every subarray with 0, 1, 2, ..., or k-1 distinct elements. Every subarray counted by atMost(k) but not by atMost(k-1) must have exactly k distinct elements. It has at most k (so it is in the first set) but more than k-1 (so it is not in the second). The difference isolates exactly k. Each atMost pass is a standard counting template, so the total work is two O(n) passes.

Exactly K Distinct - Full Implementation
1def count_exactly_k_distinct(nums, k):
2 return count_at_most_k(nums, k) - count_at_most_k(nums, k - 1)
3
4def count_at_most_k(nums, k):
5 if k < 0:
6 return 0
7 count_map = {}
8 left = 0
9 result = 0
10
11 for right in range(len(nums)):
12 count_map[nums[right]] = count_map.get(nums[right], 0) + 1
13 while len(count_map) > k:
14 count_map[nums[left]] -= 1
15 if count_map[nums[left]] == 0:
16 del count_map[nums[left]]
17 left += 1
18 result += right - left + 1
19
20 return result
21
22# Example: nums = [1, 2, 1, 2, 3], k = 2
23# count_exactly_k_distinct(nums, 2) returns 7
24# Subarrays: [1,2], [2,1], [1,2], [2,1,2], [1,2,1], [1,2,1,2], [2,3]
Interview Tip
exactly(k) = atMost(k) - atMost(k-1)
This technique is not specific to sliding window. It works anywhere "exactly k" is hard to count directly but "at most k" is easy. You will see the same identity in combinatorics, DP counting, and bit manipulation problems. The underlying principle: exactly(k) = atMost(k) - atMost(k-1) always holds when atMost is a function that can only stay the same or increase as k increases.

Sliding Window vs Other Techniques

7a: Sliding Window vs Two Pointers

Both use two indices. The distinction: sliding window tracks aggregate state across a range, while two pointers compare or coordinate values at two positions.

Sliding WindowTwo Pointers
What indices representBoundaries of a range. Everything inside mattersTwo positions being compared or coordinated
What you trackAggregate state between pointers (sum, counts, set)Relationship between elements at the pointers (sum, distance)
MovementRight always advances. Left catches up on violationEither pointer moves based on comparison
Typical inputUnsorted array or stringOften sorted array, but also linked lists and partitioning problems

Sliding Window vs Two Pointers

Sliding Window
Range: [0..2]
Sum: 6
12345678leftright
Contiguous subarray
Two Pointers
L=1, R=8
Sum: 9
12345678LR
Independent pointers

Sliding Window

All elements between pointers matter

Window slides as a unit

Two Pointers

Only values at endpoints matter

Pointers move independently

Step 1 of 6

ProblemPatternWhy
"Longest substring without repeating chars"Sliding windowTracking all chars in range [left, right]
"Two sum in sorted array"Two pointersComparing arr[left] + arr[right] to target
"Container with most water"Two pointersComparing heights at left and right
"Minimum window substring"Sliding windowTracking char counts in range
"3Sum"Two pointers (inner loop)Finding pairs that sum to complement

Ask yourself these two questions:

  1. "Am I tracking everything inside a range, or only comparing values at two positions?" If you need aggregate state (sum, count, set of elements) across all elements between the pointers, that is sliding window. If you only care about the values at the two pointer positions, that is two pointers.
  2. "Does my input need to be sorted for the logic to work?" Two pointers on sorted data is a strong signal. Sliding window almost never requires sorted input.

7b: Sliding Window vs Prefix Sums

Both handle subarray queries in O(n). The choice depends on whether the constraint is monotonic and whether negatives are present.

Sliding WindowPrefix Sums + Hash Map
Works with negativesNo (breaks monotonicity)Yes
Constraint type"at most", "at least" (monotonic)"exactly k", any sum target
SpaceO(1) to O(alphabet)O(n) for prefix map
When to chooseNonneg + at-most/at-leastNegatives present, or exact-sum needed

Ask yourself: "Can the array contain negative numbers?" If yes, sliding window on sums fails because shrinking can increase the sum instead of decreasing it. Use prefix sums + hash map. If the array is nonnegative and the constraint is "at most" or "at least," sliding window is simpler and uses less space.

7c: Sliding Window vs Center Expansion

"Longest palindromic substring" looks like a window problem. It's contiguous. It's an optimization over substrings. But Property 2 fails: you cannot update "is palindrome" in O(1) when one element enters or leaves a boundary. Adding a character to the right doesn't tell you anything about whether the extended string is palindromic without checking the matching character on the other end. Use center expansion (O(n²)) or Manacher's algorithm (O(n)).

Quick Recognition Table

ProblemTechniqueReason
"Max sum of k consecutive elements"Sliding window (fixed)Contiguous range, window size given explicitly, sum updates in O(1) per step
"Longest substring without repeats"Sliding window (longest)Substring = contiguous range. "No repeating" is an at-most constraint (at most 1 of each char). Set membership is O(1)
"Two numbers that sum to target"Two pointers / hash mapNot a contiguous range. You pick two positions anywhere. No window state to maintain
"Subarray sum = k" with negativesPrefix sums + hash mapNegatives mean shrinking can increase the sum, breaking Property 3. Prefix sums avoid this problem
"Min window containing all of t"Sliding window (shortest)Contiguous range with an "at least" constraint (contain all of t). Char count updates are O(1)
"Longest palindromic substring"Center expansion / DPAdding one character to a boundary tells you nothing about palindrome-ness without checking the opposite end. Property 2 fails
"Count subarrays with sum = k" (nonneg)Sliding window (counting)Contiguous range, nonnegative values preserve monotonicity, and the goal is counting (not optimizing)

Advanced: Monotonic Deque

The fixed-window template assumes O(1) state updates. For sums, that works: add the new element, subtract the old. But for max or min over a window, removing the current maximum does not reveal the second-largest without rescanning the window. That costs O(k) per step, making the algorithm O(nk).

The monotonic deque solves this. "Monotonic" means the values in the deque are always in decreasing order (for max problems) or increasing order (for min problems). The deque stores indices of elements that could still become the window's maximum. It maintains decreasing order by popping any element from the back that is smaller than the new element. Those popped elements will never become the window maximum. The new element is both larger and newer, so it will remain in the window longer than any of them.

Problem: Given an array and window size k, return the maximum value in each window position as it slides left to right.

Monotonic Deque (k=3)

Invariant: deque holds indices in strictly decreasing order of values
Status
INIT
Index
Value
Deque
[]
Window Max
Result
[]
1[0]3[1]-1[2]-3[3]5[4]3[5]6[6]7[7]
Deque (front = max, back = newest)
empty

Step 1 of 17 · Goal: maintain a deque of candidate indices in decreasing value order. The front is always the window max.

Every index in the deque corresponds to a value that could still become the window maximum. When a new element enters, any element already in the deque with a smaller value will never become the maximum, because the new element is both larger and newer. Pop those from the back. When the window slides forward, any index outside the window boundary is no longer valid. Pop those from the front. After both operations, the front of the deque holds the current window maximum.

Sliding Window Maximum
1from collections import deque
2
3def maxSlidingWindow(nums, k):
4 dq = deque() # stores indices, values in decreasing order
5 result = []
6
7 for i, val in enumerate(nums):
8 # Expire indices outside the window
9 while dq and dq[0] < i - k + 1:
10 dq.popleft()
11
12 # Pop indices whose values can never be max while val exists
13 while dq and nums[dq[-1]] <= val:
14 dq.pop()
15
16 dq.append(i)
17
18 # First complete window at index k-1
19 if i >= k - 1:
20 result.append(nums[dq[0]])
21
22 return result
Interview Tip
O(n) despite nested loops
Each element enters the deque once and leaves at most once. Total popleft and pop operations across the entire array are bounded by n, so it's O(n) total.
Common Mistake
Storing values instead of indices
The deque must store indices, not values. You need the index to check whether an element has expired from the window. Storing only values makes it impossible to distinguish two occurrences of the same number or verify window membership.

Common Pitfalls

Bug 1: Window Never Shrinks

Broken code
1def longest_substring_no_repeat(s):
2 seen = set()
3 left = 0
4 best = 0
5
6 for right in range(len(s)):
7 seen.add(s[right])
8 best = max(best, right - left + 1)
9
10 return best

The trap: This code never moves left. The set grows to include every character in the string, and best measures the entire string regardless of duplicates. On "abcabcbb", it returns 8 instead of 3 because the window [0, 7] contains repeated characters but is never shrunk.

Fix: The shrink loop must run before measuring. When s[right] is already in seen, remove characters from the left until the duplicate is gone, then add s[right] and measure:

1for right in range(len(s)):
2 while s[right] in seen:
3 seen.remove(s[left])
4 left += 1
5 seen.add(s[right])
6 best = max(best, right - left + 1)

Bug 2: Off-by-One in Window Size

Broken code
1best = max(best, right - left) # Missing +1

The trap: A window from index 2 to index 5 contains 4 elements, not 3. Inclusive ranges always have end - start + 1 elements. Writing right - left undercounts by one, which typically makes every answer exactly one less than correct.

Fix: Use right - left + 1.

Bug 3: Not Deleting Zero-Count Keys

Broken code
1while len(count) > k:
2 count[s[left]] -= 1 # Decrement...
3 left += 1 # ...but don't delete zero-count keys!

The trap: After decrementing count[s[left]], the key still exists in the dictionary with value 0. len(count) counts that key, so the distinct count stays inflated even though the character is no longer in the window. The shrink loop keeps running past where it should stop, or the window stays flagged as invalid when it is actually valid.

Fix: Delete the key when its count reaches zero so that len(count) accurately reflects the number of distinct characters currently in the window:

1count[s[left]] -= 1
2if count[s[left]] == 0:
3 del count[s[left]]
4left += 1

Bug 4: Wrong Shrink Direction

Broken code
1# Goal: find SHORTEST subarray with sum >= target
2while window_sum < target: # WRONG: shrinking when INVALID
3 window_sum -= nums[left]
4 left += 1

The trap: Find Shortest problems require shrinking while the window is valid, not while it is invalid. Writing while window_sum < target shrinks when the window is invalid (sum below target), which moves left forward when the sum is already too small. That is exactly backwards.

Fix: Every valid window is a candidate answer, and shorter valid windows are better, so you keep shrinking as long as validity holds. The correct condition is while window_sum >= target. Record the candidate length, then shrink to search for something shorter:

1while window_sum >= target:
2 best = min(best, right - left + 1)
3 window_sum -= nums[left]
4 left += 1

Bug 5: Uninitialized State

Broken code
1def max_sum_subarray(nums, k):
2 for right in range(len(nums)):
3 window_sum += nums[right] # never initialized!
4 if right >= k - 1:
5 best = max(best, window_sum)
6 window_sum -= nums[right - k + 1]
7 return best

The trap: Python raises UnboundLocalError (or in other languages, uses garbage or zero). The window sum accumulates incorrectly from the first iteration, and best may never update correctly.

Fix: Initialize window_sum = 0 and best = 0 (or float('-inf') if values can be negative) before the loop.

Bug 6: Wrong Data Structure

Broken code
1seen = []
2for right in range(len(s)):
3 while s[right] in seen: # O(n) lookup in list!
4 seen.remove(s[left])
5 left += 1
6 seen.append(s[right])

The trap: List membership (x in list) scans every element, costing O(n) per check. Inside the shrink loop, this turns the O(n) algorithm into O(n²). This is exactly the failure that Property 2 warns about. Without O(1) state updates, the linear-time guarantee disappears.

Fix: Use set() for O(1) membership.

Edge Cases to Always Check

Empty array

If the array has no elements, the for loop never runs. Return 0 or an empty result. The danger is accessing arr[0] or initializing state from the first element before checking length.

k = 0

For "at most 0 distinct" or "window of size 0," the shrink condition is immediately true and left can advance past right if you are not careful. Guard against this: if k equals 0, return 0 without entering the loop, or ensure your shrink loop handles the case where left > right.

k larger than the array

The window never needs to shrink because the constraint is never violated. If your code assumes at least one shrink happens (for example, uses left after the loop without checking), it may return wrong results. The entire array is the answer.

All elements identical

The window may grow to the full array without ever triggering the shrink loop. If your logic depends on shrinking happening at least once (for example, initializing best inside the shrink loop only), it never updates the answer.

No valid window exists

For "find shortest" with best initialized to float('inf'), if no window ever satisfies the constraint, best stays at infinity. Check for this before returning and convert to 0 or -1 as the problem requires.

Single element

The window is [0, 0] with size 1. If your off-by-one calculation gives size 0, this catches it. Also tests whether your initialization handles the case where left equals right.

Interview Tip
Edge Case Protocol
After coding, trace through: (1) empty input, (2) single element, (3) all same elements, (4) k = 0 or k = 1. Most bugs surface in these cases, not the happy path.

Practice Problems

Problems are organized by template type. Fixed Window is the simplest (window size is given, no shrink decision). Find Longest and Find Shortest differ only in shrink direction and update timing. Counting builds on Find Longest by accumulating valid subarray counts at each position.

Fixed Window

ProblemDifficultyApproach
Maximum Sum Subarray of Size KEasySlide a window of exactly size k. Add incoming element, subtract outgoing. Track max sum as window moves.
Maximum Average Subarray IEasySame as max sum but divide by k at the end. Track sum, not average, to avoid floating point during iteration.
Contains Duplicate IIEasyWindow = set of elements within k distance. If new element already in window, duplicate found. Remove oldest when window exceeds k.
Sliding Window MaximumHardMonotonic deque: keep indices of potentially-maximum elements in decreasing order. Remove from front when out of window, remove from back when smaller than current.

Variable Window: Find Longest

ProblemDifficultyApproach
Longest Substring Without Repeating CharactersMediumExpand right, track char positions in map. When duplicate found, shrink left past the previous occurrence. Update max after shrinking.
Longest Substring with At Most K DistinctMediumExpand right, track char frequencies. When distinct count exceeds k, shrink left until count <= k. Update max when valid.
Longest Repeating Character ReplacementMediumTrack frequency of most common char in window. If window_size - max_freq > k, shrink. The rest can be replaced.
Max Consecutive Ones IIIMediumReframe: longest window with at most k zeros. Expand right, count zeros. When zeros > k, shrink left. Update max when valid.
Fruit Into BasketsMediumLongest subarray with at most 2 distinct elements. Same pattern as "at most k distinct" with k=2.

Variable Window: Find Shortest

ProblemDifficultyApproach
Minimum Size Subarray SumMediumExpand until sum >= target. Then shrink while still valid, updating min at each step. Update before shrinking, not after.
Minimum Window SubstringHardTrack required char counts. Expand until all required chars satisfied. Shrink while valid, updating min. Use "formed" counter to avoid rechecking all chars.

Counting

ProblemDifficultyApproach
Subarrays with K Different IntegersHardExactly k = atMost(k) - atMost(k-1). Each atMost(k) is a "find longest" variant that counts all valid subarrays ending at each position.
Count Number of Nice SubarraysMediumReframe: subarrays with exactly k odd numbers. Use atMost(k) - atMost(k-1). Or use prefix sum approach with hash map.
Binary Subarrays With SumMediumSubarrays summing to exactly goal. Use atMost(goal) - atMost(goal-1). Each atMost counts subarrays with sum <= target.

Test Your Understanding

10 questions covering sliding window patterns and edge cases.

Practice this pattern

Apply the guide to complete interview problems with explanations and code.

Learn Sliding Window in a guided sequence

The Interview Course connects this pattern to its prerequisites, worked lessons, and progressively harder problems.

Explore the course