Coding patternsArrays and strings
Sliding window
The sliding window coding pattern for interviews, fixed and variable size. How to spot it, the grow-and-shrink template, a step-through of longest substring without repeats, code in Java, Python, C#, Go and TypeScript, and 10 LeetCode problems.
- Read first
- Two pointers
“Find the longest substring with no repeated letter.” For a text of n letters, the brute force
tries every start and every end. That’s O(n²) windows, and it checks each one, which can make it
O(n³). But neighboring windows share almost everything. The window from 2 to 7 and the window from 2
to 8 differ by one letter. So rechecking all of it is wasted work.
A sliding window keeps one window, [left, right], and the facts about it (a set of letters,
a sum, a count per letter). Moving right adds one element, moving left removes one, and each
update is O(1). Both edges only move forward, so the whole scan is O(n). It’s
two pointers where the pointers bound a range instead of marking
a pair.
How to spot it
Section titled “How to spot it”- “Contiguous” or “substring” or “subarray” together with “longest”, “shortest”, “maximum” or “minimum”.
- A rule about what’s inside the window: at most k distinct letters, no repeats, sum at least target, contains every letter of another string.
- A fixed length k in the statement: averages, max sum of k consecutive elements, anagrams of a pattern inside a string.
It is not a sliding window when the elements don’t have to be next to each other. That’s usually dynamic programming. It’s also not one when the rule isn’t monotonic, such as “subarray sum exactly k” with negative numbers. That’s prefix sums.
Watch it run
Section titled “Watch it run”Longest stretch of "abcbadcb" with no repeated letter. The yellow band is the window. When
right reaches a letter that’s already inside, the window is broken, and left moves forward
until the old copy is gone. Only then is the length measured.
- seen = set(); left = best = 0
- for right, letter in enumerate(text):
- while letter in seen: # broken: shrink
- seen.remove(text[left]); left += 1
- seen.add(letter) # valid again
- best = max(best, right - left + 1)
Find the longest stretch of "abcbadcb" with no repeated letter. The window is everything from left to right.
Count the moves. right moves 8 times, and left moves at most 8 times. The while loop inside the
for loop looks like O(n²). But left never moves backward, so across the whole run it does at
most n steps in total. That’s called an “amortized” argument. It’s what you say when an interviewer
asks about the nested loop.
The template
Section titled “The template”The two shapes of variable window: longest valid (shrink while invalid) and shortest valid (shrink while valid).
import java.util.HashSet;import java.util.Set;
public class SlidingWindow { static int longestUnique(String text) { // Longest valid window: grow right every step; when the new character breaks the rule // ("no repeats"), shrink from the left until it holds again. Each index enters once and // leaves at most once, so this is O(n) even with the inner while. Set<Character> seen = new HashSet<>(); int left = 0, best = 0; for (int right = 0; right < text.length(); right++) { char letter = text.charAt(right); while (seen.contains(letter)) seen.remove(text.charAt(left++)); seen.add(letter); best = Math.max(best, right - left + 1); // measured only while valid } return best; }
static int minSubarrayLen(int target, int[] nums) { // Shortest valid window: grow until the rule holds ("sum >= target"), then shrink // while it still holds, recording each valid length. Correct only because every number // is positive: adding grows the sum and removing shrinks it. int left = 0, total = 0, best = Integer.MAX_VALUE; for (int right = 0; right < nums.length; right++) { total += nums[right]; while (total >= target) { best = Math.min(best, right - left + 1); total -= nums[left++]; } } return best == Integer.MAX_VALUE ? 0 : best; }
public static void main(String[] args) { check(longestUnique("abcbadcb") == 4); check(longestUnique("") == 0); check(longestUnique("bbbb") == 1); check(minSubarrayLen(7, new int[] {2, 3, 1, 2, 4, 3}) == 2); check(minSubarrayLen(100, new int[] {1, 2, 3}) == 0); System.out.println("ok: longestUnique and minSubarrayLen"); }
static void check(boolean ok) { if (!ok) throw new AssertionError(); }}def longest_unique(text: str) -> int: # Longest valid window: grow right every step; when the new letter breaks the rule # ("no repeats"), shrink from the left until it holds again. Each index enters the window # once and leaves at most once, so this is O(n) even with the inner while. seen: set[str] = set() left = best = 0 for right, letter in enumerate(text): while letter in seen: seen.remove(text[left]) left += 1 seen.add(letter) best = max(best, right - left + 1) # measured only while the window is valid return best
def min_subarray_len(target: int, nums: list[int]) -> int: # Shortest valid window: grow until the rule holds ("sum >= target"), then shrink while it # still holds, recording each valid length. Only correct because every number is positive: # adding grows the sum and removing shrinks it, so the window can't skip an answer. left = total = 0 best = len(nums) + 1 for right, number in enumerate(nums): total += number while total >= target: best = min(best, right - left + 1) total -= nums[left] left += 1 return best if best <= len(nums) else 0
assert longest_unique("abcbadcb") == 4assert longest_unique("") == 0assert longest_unique("bbbb") == 1assert min_subarray_len(7, [2, 3, 1, 2, 4, 3]) == 2assert min_subarray_len(100, [1, 2, 3]) == 0print("ok: longest_unique and min_subarray_len")using System;using System.Collections.Generic;
static class SlidingWindow{ public static int LongestUnique(string text) { // Longest valid window: grow right every step; when the new character breaks the rule // ("no repeats"), shrink from the left until it holds again. Each index enters once and // leaves at most once, so this is O(n) even with the inner while. var seen = new HashSet<char>(); int left = 0, best = 0; for (int right = 0; right < text.Length; right++) { char letter = text[right]; while (seen.Contains(letter)) seen.Remove(text[left++]); seen.Add(letter); best = Math.Max(best, right - left + 1); // measured only while valid } return best; }
public static int MinSubarrayLen(int target, int[] nums) { // Shortest valid window: grow until the rule holds ("sum >= target"), then shrink // while it still holds, recording each valid length. Correct only because every number // is positive: adding grows the sum and removing shrinks it. int left = 0, total = 0, best = int.MaxValue; for (int right = 0; right < nums.Length; right++) { total += nums[right]; while (total >= target) { best = Math.Min(best, right - left + 1); total -= nums[left++]; } } return best == int.MaxValue ? 0 : best; }
static void Main() { Check(LongestUnique("abcbadcb") == 4); Check(LongestUnique("") == 0); Check(LongestUnique("bbbb") == 1); Check(MinSubarrayLen(7, [2, 3, 1, 2, 4, 3]) == 2); Check(MinSubarrayLen(100, [1, 2, 3]) == 0); Console.WriteLine("ok: LongestUnique and MinSubarrayLen"); }
static void Check(bool ok) { if (!ok) throw new InvalidOperationException("check failed"); }}package main
import "fmt"
func longestUnique(text string) int { // Longest valid window: grow right every step; when the new byte breaks the rule ("no // repeats"), shrink from the left until it holds again. Each index enters once and leaves // at most once, so this is O(n) even with the inner loop. (Bytes, so ASCII input.) seen := make(map[byte]bool) left, best := 0, 0 for right := 0; right < len(text); right++ { for seen[text[right]] { delete(seen, text[left]) left++ } seen[text[right]] = true best = max(best, right-left+1) // measured only while valid } return best}
func minSubarrayLen(target int, nums []int) int { // Shortest valid window: grow until the rule holds ("sum >= target"), then shrink while it // still holds, recording each valid length. Correct only because every number is // positive: adding grows the sum and removing shrinks it. left, total, best := 0, 0, len(nums)+1 for right, number := range nums { total += number for total >= target { best = min(best, right-left+1) total -= nums[left] left++ } } if best > len(nums) { return 0 } return best}
func main() { check(longestUnique("abcbadcb") == 4) check(longestUnique("") == 0) check(longestUnique("bbbb") == 1) check(minSubarrayLen(7, []int{2, 3, 1, 2, 4, 3}) == 2) check(minSubarrayLen(100, []int{1, 2, 3}) == 0) fmt.Println("ok: longestUnique and minSubarrayLen")}
func check(ok bool) { if !ok { panic("check failed") }}function longestUnique(text: string): number { // Longest valid window: grow right every step; when the new character breaks the rule // ("no repeats"), shrink from the left until it holds again. Each index enters once and // leaves at most once, so this is O(n) even with the inner while. const seen = new Set<string>(); let left = 0; let best = 0; for (let right = 0; right < text.length; right++) { while (seen.has(text[right])) seen.delete(text[left++]); seen.add(text[right]); best = Math.max(best, right - left + 1); // measured only while valid } return best;}
function minSubarrayLen(target: number, nums: number[]): number { // Shortest valid window: grow until the rule holds ("sum >= target"), then shrink while it // still holds, recording each valid length. Correct only because every number is positive: // adding grows the sum and removing shrinks it. let left = 0; let total = 0; let best = Infinity; for (let right = 0; right < nums.length; right++) { total += nums[right]; while (total >= target) { best = Math.min(best, right - left + 1); total -= nums[left++]; } } return best === Infinity ? 0 : best;}
const check = (ok: boolean) => { if (!ok) throw new Error("check failed");};check(longestUnique("abcbadcb") === 4);check(longestUnique("") === 0);check(longestUnique("bbbb") === 1);check(minSubarrayLen(7, [2, 3, 1, 2, 4, 3]) === 2);check(minSubarrayLen(100, [1, 2, 3]) === 0);console.log("ok: longestUnique and minSubarrayLen");Write any variable window by answering three questions first:
- What state do I keep? A set, a count map, a running sum, the number of letters still missing.
- When is the window invalid (for longest) or valid (for shortest)? This must be O(1) to check from the state.
- When do I record the answer? For longest, after shrinking, when the window is valid again. For shortest, inside the shrink loop, while it’s still valid.
Fixed-size windows
Section titled “Fixed-size windows”When the length is fixed at k (window_size in code), there’s no shrink loop. Add nums[right],
and once right >= window_size, remove nums[right - window_size]. Permutation in string is this
shape: a window the length of the pattern, compared by letter counts, where each slide changes two
counts.
Tracking “how many are still missing”
Section titled “Tracking “how many are still missing””Minimum window substring asks for the shortest window containing every letter of target_text, with
duplicates. Comparing two count maps at every step is O(26) or O(distinct letters). Instead keep one
number, missing, the count of required letters not yet covered. It drops when a needed letter
enters and rises when one leaves, so validity is missing == 0, an O(1) check.
Complexity
Section titled “Complexity”Every variable window is O(n) time, because each index enters once and leaves at most once. Memory is the size of the state: O(1) for a sum, O(k) for a set of up to k distinct items, O(alphabet) for letter counts.
Sliding window maximum is the exception that needs more. It asks for the largest value in each window of size k. A max can’t be “removed” in O(1) when it slides out. So the window keeps a monotonic deque of candidates instead. It’s the same idea as the monotonic stack, and it stays O(n).
The answer is a contiguous range, and a window that breaks the rule only gets worse as it grows, so I’ll slide a window. Both edges only move forward, so it’s O(n) even with the inner while loop.
Interview questions
Section titled “Interview questions”Interviewer asksYour solution has a while loop inside a for loop. Isn't that O(n²)?
No. Each iteration of the inner loop moves left forward by one, and left never moves back or
passes the end. So across the whole run the inner loop runs at most n times in total. The total
work is O(n) for right plus O(n) for left.
Interviewer asksLongest substring with at most k distinct characters: what's the state and the rule?
A map from character to count inside the window. The window is invalid when the map has more than k keys. When shrinking, decrement the left character’s count and delete the key when it reaches 0, so the number of keys stays accurate. Record the length after shrinking.
Interviewer asksLongest repeating character replacement: why don't you decrease the max count when shrinking?
The answer only improves when a window has a higher max count than any window before it. A stale, too-high max count can make the window look valid when it isn’t. But it only keeps the window from shrinking. The length it reports was already reached earlier. So the answer is still correct, and you skip an O(26) rescan on every step. This one is subtle. Saying why it’s safe is a strong signal.
Practice
Section titled “Practice”10 problems on LeetCode: 2 easy, 6 medium, 2 hard
- Best Time to Buy and Sell Stock(opens LeetCode in a new tab)Easy
Keep the cheapest day so far as the left edge.
- Maximum Average Subarray I(opens LeetCode in a new tab)Easy
A fixed window: add the new element, drop the old one.
- Longest Substring Without Repeating Characters(opens LeetCode in a new tab)Medium
Shrink from the left until the new character is unique again.
- Longest Repeating Character Replacement(opens LeetCode in a new tab)Medium
Valid while window length minus the top count is at most k.
- Permutation in String(opens LeetCode in a new tab)Medium
A fixed window the length of s1, compared by letter counts.
- Minimum Size Subarray Sum(opens LeetCode in a new tab)Medium
Grow until the sum is big enough, then shrink while it still is.
- Max Consecutive Ones III(opens LeetCode in a new tab)Medium
The window is valid while it holds at most k zeros.
- Fruit Into Baskets(opens LeetCode in a new tab)Medium
Longest window with at most two distinct values.
- Minimum Window Substring(opens LeetCode in a new tab)Hard
Track how many required letters are still missing, not the whole map.
- Sliding Window Maximum(opens LeetCode in a new tab)Hard
A monotonic deque keeps the window maximum at its front.
Ticks are saved in this browser only. Clear your site data and they're gone.
- Use it when the answer is a contiguous subarray or substring, and the question is longest, shortest or a fixed length k.
- State: whatever answers “is this window valid?” in O(1).
- Longest: grow, shrink while invalid, record. Shortest: grow, record and shrink while valid. Fixed: add one, remove one.
- Cost: O(n) time, since both edges only move forward.
- Check first: the rule must be monotonic. With negative numbers, use prefix sums instead.
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.