~/searching/sliding-window
Sliding window
Find the best contiguous stretch of an array or string in one pass: grow a window on the right, shrink it from the left.
Keep a window [left, right]. Move right one step at a time; while the window breaks the rule, move left. Record the answer after each step.
You want the longest or shortest contiguous subarray or substring with a property that only gets harder to keep as the window grows.
O(n)
O(min(n, alphabet size))
You’ll recognise it when
- The answer is a contiguous piece: a subarray or a substring, not any subset.
- You want the longest or shortest piece with some property: no repeats, at most
kdistinct values, sum at leastS. - If a window is fine, every smaller window inside it is fine too (or, for “shortest” problems, every bigger one is).
- The brute force tries every start and every end, which is O(n²) or worse.
It’s often confused with prefix sums, which answer “what’s the sum of this range?” for any range. A window only moves forward, so it needs a rule that behaves nicely as it grows and shrinks.
The idea
Picture reading a long receipt through a cardboard tube you can stretch. You slide the far end forward one line at a time. If the tube now shows something it shouldn’t, say the same item twice, you pull the near end forward until it doesn’t. Neither end ever goes back.
That’s the whole technique. Two indexes, left and right, mark the window. right visits every position once. left only moves when the window breaks the rule, and it only moves forward. After each step the window is the longest valid window ending at right, so the best of those is the answer.
How it works
Here’s the pattern on one problem: the length of the longest substring with no repeated character. A count table says how many copies of each letter are inside the window.
Scroll through the steps and the graphic follows along. You can also press play, step with the arrow keys, or edit the string: try abba, aaaa, or a word with no repeats.
- Start with an empty window:
left = 0,best = 0, and an emptycount. - Expand: move
rightone step and add its letter,count[ch] += 1. - If
count[ch]is now 2, the window has a repeat, and it’s exactly the letter you just added. - Shrink: drop
s[left]fromcountand moveleftforward, untilcount[ch]is back to 1. Here “backp” + ‘a’ has to lose both ‘b’ and the old ‘a’. - Record: the window
[left, right]has no repeats, sobest = max(best, right - left + 1). A shorter window than before is normal;bestremembers the longest one seen. - When
righthas passed the end,bestis the answer: “packing”, length 7.
Why it’s correct: after each step, the window is the longest one ending at right with no repeats. It can’t start earlier, because left only moved past a letter when keeping it meant a repeat, and a window that contains a repeat still contains it when you make it longer. Every substring ends somewhere, so checking the best window at each right checks them all.
def longest_unique(s):
"""Length of the longest substring of s with no repeated character."""
count = {} # letter -> copies inside the window
left = best = 0
for right, ch in enumerate(s):
count[ch] = count.get(ch, 0) + 1
while count[ch] > 1:
count[s[left]] -= 1
left += 1
best = max(best, right - left + 1)
return best#include <string>
#include <algorithm>
using namespace std;
// Length of the longest substring of s with no repeated character (ASCII).
int longestUnique(const string& s) {
int count[128] = {}; // copies of each char inside the window
int left = 0, best = 0;
for (int right = 0; right < (int)s.size(); right++) {
char ch = s[right];
count[ch]++;
while (count[ch] > 1) {
count[s[left]]--;
left++;
}
best = max(best, right - left + 1);
}
return best;
}class LongestUnique {
// Length of the longest substring of s with no repeated character (ASCII).
static int longestUnique(String s) {
int[] count = new int[128]; // copies of each char inside the window
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char ch = s.charAt(right);
count[ch]++;
while (count[ch] > 1) {
count[s.charAt(left)]--;
left++;
}
best = Math.max(best, right - left + 1);
}
return best;
}
}The same skeleton solves most window problems. Only the rule changes:
left = 0
for right in range(len(a)):
add(a[right]) # expand
while window_is_invalid():
remove(a[left]) # shrink
left += 1
best = max(best, right - left + 1) # record
When does it work? The rule must be monotone: if a window breaks it, every bigger window containing it breaks it too. “No repeats”, “at most k distinct” and “sum ≤ S with non-negative numbers” all qualify. “Sum ≤ S” with negative numbers doesn’t: adding a negative number can make a bad window good again, so shrinking can throw away the answer. Use prefix sums (with a sorted structure or a monotonic deque) for those.
Why it’s O(n)
There’s a loop inside a loop, but count how far the pointers move instead. right goes from 0 to n - 1 once. left also only goes forward, and never passes right + 1, so across the whole run the inner loop body runs at most n times in total.
| work | total over the whole run |
|---|---|
right steps (expand) |
n |
left steps (shrink) |
at most n |
count updates |
at most 2n |
That’s O(n) time. The count table holds one entry per distinct letter in the window: at most 26 for lowercase letters, 128 for ASCII.
Common mistakes
Letting left jump backwards
A popular speed-up stores the last index of each letter and jumps left straight past it. But the last copy may already be outside the window. On “abba”, the final ‘a’ was last seen at 0, so left jumps back from 2 to 1 and “bba” is wrongly counted.
left = last[ch] + 1 # ✗ can move left backwards
left = max(left, last[ch] + 1) # ✓ the window only moves forward
Recording before the window is valid
Update best only after the shrink loop, when the window follows the rule again. Updating right after adding a[right] counts windows with a repeat in them.
Counting distinct values with a map that never shrinks
For “at most k distinct”, you check len(count) > k. That only works if you delete a key when its count drops to 0. Otherwise the map keeps every value it has ever seen.
count[a[left]] -= 1 # ✗ zero entries still counted
if count[a[left]] == 0: del count[a[left]] # ✓ len(count) is the distinct count
Using a window when the rule isn’t monotone
With negative numbers, a sum rule like “sum at most S” or “sum exactly k” breaks the window: a bad window can turn good again as it grows, so shrinking can skip the answer. Switch to prefix sums, for example with a hash map of earlier sums.
Variations
- Fixed-size window. For every window of exactly
kelements (a moving average, say), adda[right]and removea[right - k]on every step. No shrink loop needed. - Minimum window. For the shortest valid window, like the smallest substring containing all of
t‘s letters, flip the loop: grow until valid, then shrink while it’s still valid, recording the answer inside the shrink loop. - At most
kdistinct. Same template withlen(count) > kas the rule. “Exactlykdistinct” isn’t monotone, but it equals atMost(k) − atMost(k− 1). - Sliding window maximum. For the max of every window of size
k, keep a deque of indexes whose values decrease from front to back. Pop smaller values off the back before pushing, and pop the front when it falls out of the window. The front is always the max: O(n) overall. - Jump with last-seen indexes. For the no-repeat problem, store each letter’s last index and set
left = max(left, last[ch] + 1). Same O(n), one jump instead of a loop.
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
A row of trees, each with a fruit type. You may pick from one contiguous stretch of trees, holding at most 2 fruit types. What’s the longest stretch? Which approach fits?
The answer is contiguous, and “at most 2 types” is monotone: if a stretch has 3 types, any longer stretch containing it does too. So grow on the right and shrink on the left for O(n). Checking every pair of ends is O(n²), and sorting destroys the contiguity the problem is about.
-
2
This version jumps
leftusing each letter’s last index. What does it print?def longest(s):last, left, best = {}, 0, 0for right, ch in enumerate(s):if ch in last:left = last[ch] + 1last[ch] = rightbest = max(best, right - left + 1)return bestprint(longest("abba"), longest("abcb"))On “abba”, the final ‘a’ was last seen at index 0, so
leftjumps back from 2 to 1 and “bba”, which has a repeat, is counted as 3. The true answer is 2. On “abcb” the jump happens to go forward, so the 3 there is correct. A bug that only shows on some inputs is exactly why you test with “abba”. The fix isleft = max(left, last[ch] + 1). -
3
at_most(a, k)counts the subarrays with at mostkdistinct values. What does this print?def at_most(a, k):count, left, total = {}, 0, 0for right, x in enumerate(a):count[x] = count.get(x, 0) + 1while len(count) > k:count[a[left]] -= 1if count[a[left]] == 0:del count[a[left]]left += 1total += right - left + 1return totala = [2, 1, 2, 3]print(at_most(a, 2), at_most(a, 2) - at_most(a, 1))Adding
right - left + 1counts every valid subarray ending atright. Of the 10 subarrays, only [1, 2, 3] and [2, 1, 2, 3] have 3 distinct values, so at_most(2) = 8. No two neighbours are equal, so at_most(1) = 4, the single elements. The difference, 4, counts subarrays with exactly 2 distinct values: [2, 1], [1, 2], [2, 3] and [2, 1, 2]. “Exactly k” isn’t monotone by itself, which is why this subtraction trick exists. -
4
The window template has a
whileloop inside aforloop overnelements. What’s the running time?Count pointer moves instead of loop nesting.
rightmakes n steps andleftmakes at most n steps in total, so the total work is O(n). The O(n²) answer would be right only ifleftcould move back. -
5
“Longest subarray with sum at most S”. The window version works when every number is non-negative. Why does it fail when some numbers are negative?
The window relies on a monotone rule: once a window is too big, every longer one is too. With negatives that’s false, e.g. [5, -4] has a smaller sum than [5]. Use prefix sums instead, for example with a sorted structure or a monotonic deque.
Practice problems
- easy Maximum Average Subarray I leetcode.com
- medium Longest Substring Without Repeating Characters leetcode.com
- medium Playlist cses.fi
- medium Minimum Size Subarray Sum leetcode.com
- medium Longest Repeating Character Replacement leetcode.com
- hard Minimum Window Substring leetcode.com
- hard Sliding Window Maximum leetcode.com