← back

Sliding Window

Variable-Size Sliding Window

Maintain a window with left and right pointers. Expand right to include elements; shrink left when a constraint is violated. Find the longest/shortest substring or subarray satisfying a condition.

Given a string, find the length of the longest substring that contains no repeating characters. For example, in "abcabcbb" the answer is 3, because "abc" is the longest stretch where every character is unique.

The brute-force approach is straightforward: check every possible substring, and for each one, verify whether all characters are distinct. There are O(n2)O(n^2) substrings, and checking each for uniqueness takes up to O(n)O(n) time, giving O(n3)O(n^3) overall. You can improve the uniqueness check to O(1)O(1) amortized with a hash set, bringing it down to O(n2)O(n^2), but that is still far too slow for strings of length 100,000. We need a fundamentally different approach.

The sliding window idea

Instead of checking every substring independently, maintain a window defined by two pointers, left and right, that always satisfies the constraint (in this case, all characters in the window are unique). The algorithm works in two phases that alternate: expand the window by moving right forward, and shrink the window by moving left forward whenever the constraint is violated.

The reason this works is that once a window is invalid, no larger window starting at the same left position can possibly be valid. So instead of restarting from scratch, you slide the left edge forward until the window is valid again, then continue expanding.

Walking through "abcabcbb"

Let's trace the algorithm on the string "abcabcbb" step by step. We maintain a set of characters in the current window.

We start with left = 0 and right = 0. We add 'a' to the set. Window: "a", length 1. Best so far: 1.

Move right to 1. Add 'b'. Window: "ab", length 2. Best: 2.

Move right to 2. Add 'c'. Window: "abc", length 3. Best: 3.

Move right to 3. The character is 'a', which is already in the set. The constraint is violated, so we shrink from the left. Remove 'a' at left = 0, advance left to 1. Now the set is {b, c} and 'a' is no longer a duplicate, so we add 'a' and stop shrinking. Window: "bca", length 3. Best: 3.

Move right to 4. Character is 'b', already in the set. Shrink: remove 'b' at left = 1, advance left to 2. Now add 'b'. Window: "cab", length 3. Best: 3.

Move right to 5. Character is 'c', already in the set. Shrink: remove 'c' at left = 2, advance left to 3. Add 'c'. Window: "abc", length 3. Best: 3.

Move right to 6. Character is 'b', already in the set. Shrink: remove 'a' at left = 3, advance left to 4. 'b' is still in the set. Remove 'b' at left = 4, advance left to 5. Now add 'b'. Window: "cb", length 2. Best still 3.

Move right to 7. Character is 'b', already in the set. Shrink: remove 'c' at left = 5, advance left to 6. Now add 'b'. Window: "b", length 1. Best still 3.

The answer is 3.

Code for longest substring without repeating characters

1
2
3
4
5
6
7
8
9
10
11
def lengthOfLongestSubstring(s):
    char_set = set()
    left = 0
    best = 0
    for right in range(len(s)):
        while s[right] in char_set:
            char_set.remove(s[left])
            left += 1
        char_set.add(s[right])
        best = max(best, right - left + 1)
    return best

The outer loop advances right one step at a time. The inner while loop shrinks from the left only when needed. After the while loop exits, the window is valid again, so we update the answer.

Why this is O(n)O(n)

It might look like the nested loops make this O(n2)O(n^2), but consider what happens to each element. Every character is added to the set exactly once (when right reaches it) and removed from the set at most once (when left passes it). So the inner while loop does not execute n times per outer iteration. Across the entire run of the algorithm, left advances at most n times total. The total work is O(n)O(n) plus O(n)O(n) equals O(n)O(n). This amortized argument is the key to understanding why sliding window algorithms are efficient.

Minimum window substring: the shortest valid window variant

LeetCode 76 asks you to find the smallest substring of s that contains all characters of t, given strings s and t. For example, with s = "ADOBECODEBANC" and t = "ABC", the answer is "BANC".

This is still a sliding window problem, but instead of looking for the longest valid window, you want the shortest. This flips where you update the answer. For "longest" problems, you update the answer after the shrink loop (when the window is valid and as large as possible). For "shortest" problems, you update the answer inside the shrink loop (each time you shrink and the window is still valid, it might be a new minimum).

Walking through minimum window substring

With s = "ADOBECODEBANC" and t = "ABC", we need a window containing at least one A, one B, and one C. We track how many characters we still need.

Expand right through A, D, O, B, E, C. At this point the window "ADOBEC" (indices 0-5) contains all three target characters. Record length 6. Now shrink: remove A from left, move left to 1. The window "DOBEC" still needs an A, so it is no longer valid. Stop shrinking. Continue expanding.

Eventually the window reaches "CODEBA" and later "BANC". Each time the window becomes valid, we shrink as much as possible, recording the length each time the window is still valid. The shortest valid window turns out to be "BANC" with length 4.

Code for minimum window substring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from collections import Counter

def minWindow(s, t):
    need = Counter(t)
    missing = len(t)
    left = 0
    best_start, best_len = 0, float('inf')

    for right in range(len(s)):
        if need[s[right]] > 0:
            missing -= 1
        need[s[right]] -= 1

        while missing == 0:
            window_len = right - left + 1
            if window_len < best_len:
                best_start = left
                best_len = window_len
            need[s[left]] += 1
            if need[s[left]] > 0:
                missing += 1
            left += 1

    return "" if best_len == float('inf') else s[best_start:best_start + best_len]

Notice the structural difference: the answer update (if window_len < best_len) is inside the while loop. Every time we shrink and the window is still valid, we check whether it is a new minimum. This is the fundamental distinction between longest-window and shortest-window problems.

Longest substring with at most K distinct characters

Consider LeetCode 340: given a string and an integer k, you must find the length of the longest substring with at most k distinct characters. For example, in "eceba" with k = 2, the answer is 3 ("ece").

The structure is identical to the no-repeats problem. You just change what "valid" means. Instead of checking for duplicates, you check whether the number of distinct characters exceeds k.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import defaultdict

def lengthOfLongestSubstringKDistinct(s, k):
    count = defaultdict(int)
    left = 0
    best = 0
    for right in range(len(s)):
        count[s[right]] += 1
        while len(count) > k:
            count[s[left]] -= 1
            if count[s[left]] == 0:
                del count[s[left]]
            left += 1
        best = max(best, right - left + 1)
    return best

The window state is a frequency map, and the validity check is len(count) <= k. Everything else, the expand/shrink structure and the answer update outside the while loop, is the same template.

The general template

Every variable-size sliding window problem follows the same skeleton. You initialize left = 0 and some data structure tracking the window state. You iterate right across the input, updating the state. When the state violates the constraint, you shrink from the left until it is valid again. You update the answer either outside the shrink loop (for longest problems) or inside it (for shortest problems).

What changes between problems is just three things: the data structure tracking window state (a set, a frequency map, a counter), the condition that triggers shrinking (duplicate found, too many distinct characters, required characters not all present), and whether you want the longest or shortest valid window.

Once you internalize this template, you can solve a wide range of problems by simply identifying these three components. The algorithmic structure is always the same.

Longest repeating character replacement

LeetCode 424 presents a variation: given a string and an integer k, find the length of the longest substring where you can replace at most k characters to make every character the same. For example, with s = "AABABBA" and k = 1, the answer is 4 ("AABA", replacing the B at index 3).

The key insight is that a window is valid when (window_size - max_freq) <= k, where max_freq is the count of the most frequent character in the window. The quantity window_size - max_freq tells you how many characters you would need to replace.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import defaultdict

def characterReplacement(s, k):
    count = defaultdict(int)
    left = 0
    max_freq = 0
    best = 0
    for right in range(len(s)):
        count[s[right]] += 1
        max_freq = max(max_freq, count[s[right]])
        while (right - left + 1) - max_freq > k:
            count[s[left]] -= 1
            left += 1
        best = max(best, right - left + 1)
    return best

A subtle point: we never decrease max_freq when shrinking the window. This is safe because the answer can only improve when max_freq increases. If max_freq is stale (too high), the window just stays the same size or smaller, which does not produce a wrong answer. It only means we might not shrink as aggressively as we could.

Find all anagrams in a string

For LeetCode 438, given a string s and a pattern p, you must find all start indices of p's anagrams in s. For example, with s = "cbaebabacd" and p = "abc", the answer is [0, 6] because "cba" and "bac" are both anagrams of "abc".

This is a fixed-size sliding window of length len(p). Strictly speaking, it is a cousin of the variable-size template covered here, but it shows how the same "track what is missing" bookkeeping carries over. Use a single need map initialized from p's counts, decrementing when a character enters the window and incrementing when it leaves. Track how many characters are still missing; the window is a valid anagram when that count reaches zero.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from collections import Counter

def findAnagrams(s, p):
    if len(s) < len(p):
        return []
    need = Counter(p)
    missing = len(p)
    result = []
    for right in range(len(s)):
        if need[s[right]] > 0:
            missing -= 1
        need[s[right]] -= 1
        if right >= len(p):
            left_char = s[right - len(p)]
            need[left_char] += 1
            if need[left_char] > 0:
                missing += 1
        if missing == 0:
            result.append(right - len(p) + 1)
    return result

This mirrors the minimum window substring approach but with a fixed window size, which eliminates the need for an inner while loop.

Subarray product less than K

LeetCode 713 adds a twist: given an array of positive integers and a target k, you must count the number of contiguous subarrays whose product is strictly less than k. For example, with nums = [10, 5, 2, 6] and k = 100, the answer is 8.

The twist here is that instead of tracking the longest or shortest window, you count subarrays. When the window [left, right] is valid (product < k), every subarray ending at right that starts at or after left is also valid. There are exactly right - left + 1 such subarrays.

1
2
3
4
5
6
7
8
9
10
11
12
13
def numSubarrayProductLessThanK(nums, k):
    if k <= 1:
        return 0
    product = 1
    left = 0
    count = 0
    for right in range(len(nums)):
        product *= nums[right]
        while product >= k:
            product //= nums[left]
            left += 1
        count += right - left + 1
    return count

The count += right - left + 1 line is the key: for a valid window of size w ending at right, the new subarrays that end exactly at right are [left..right], [left+1..right], ..., [right..right], totaling w subarrays. We use integer division because the values are positive integers and we only care about the comparison with k.

Subarrays with K different integers

With LeetCode 992, given an integer array and a target k, you must count the number of subarrays with exactly k distinct integers. For example, with nums = [1, 2, 1, 2, 3] and k = 2, the answer is 7.

Counting "exactly K" directly with a sliding window is awkward because removing an element from the left might not reduce the distinct count (there could be duplicates). The standard trick is to decompose "exactly K" into "at most K" minus "at most K - 1". Each "at most K" subproblem is a straightforward sliding window.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from collections import defaultdict

def subarraysWithKDistinct(nums, k):
    def atMost(k):
        count = defaultdict(int)
        left = 0
        result = 0
        for right in range(len(nums)):
            if count[nums[right]] == 0:
                k -= 1
            count[nums[right]] += 1
            while k < 0:
                count[nums[left]] -= 1
                if count[nums[left]] == 0:
                    k += 1
                left += 1
            result += right - left + 1
        return result
    return atMost(k) - atMost(k - 1)

Inside atMost, the logic is identical to the "at most K distinct characters" problem, and the counting trick is the same as the product-less-than-K problem: each valid window of size w contributes w new subarrays. The subtraction cancels out every subarray with fewer than k distinct values, leaving only those with exactly k.

Count subarrays with fixed bounds

Looking at LeetCode 2444, given an integer array and two integers minK and maxK, you must count the number of subarrays where the minimum value equals minK and the maximum value equals maxK. For example, with nums = [1, 3, 5, 2, 7, 5] and minK = 1, maxK = 5, the answer is 2.

This problem does not use the expand/shrink template. Instead, scan left to right and track three indices: the last position where you saw minK, the last position where you saw maxK, and the last position where you saw a value outside the range [minK, maxK]. For each position right, the number of valid subarrays ending there is determined by how far left you can start while still including both bounds and excluding out-of-range elements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def countSubarrays(nums, minK, maxK):
    last_min = -1
    last_max = -1
    last_bad = -1
    count = 0
    for right in range(len(nums)):
        if nums[right] < minK or nums[right] > maxK:
            last_bad = right
        if nums[right] == minK:
            last_min = right
        if nums[right] == maxK:
            last_max = right
        valid_start = min(last_min, last_max)
        count += max(0, valid_start - last_bad)
    return count

The expression min(last_min, last_max) gives the earliest position that still lets both bounds be present. The subarray must start after last_bad to avoid out-of-range elements. So valid starting positions are last_bad + 1 through valid_start, giving valid_start - last_bad choices (or zero if that is negative). This runs in O(n)O(n) time with O(1)O(1) space.

Complexity analysis

Time complexity is O(n)O(n) for all standard variable-size sliding window problems. The argument is always the same: each element enters the window once and leaves at most once, so the total work across all iterations of the inner loop is O(n)O(n). The outer loop also does O(n)O(n) work, giving O(n)O(n) total.

Space complexity is O(k)O(k) where k depends on the problem. For the no-repeats problem, k is the size of the character set (at most 26 for lowercase English letters, or 128 for ASCII). For the K distinct characters problem, k is the parameter K. For minimum window substring, k is the size of the target string's character set. In all cases, the space is bounded by the alphabet or the constraint, not the input size.

Recognizing this pattern

You're probably looking at a variable-size sliding window when:

  • The problem asks for the longest or shortest contiguous subarray/substring satisfying a condition, and the condition gets strictly harder as the window grows (a monotone predicate).
  • The condition is shaped like "no repeated character," "at most k distinct," "sum \geq target," "contains all of t," "product < k," or "frequency constraint."
  • All values are non-negative (so adding an element only makes a sum/product grow), which is what makes shrinking-the-left work.
  • A brute-force "for each (l, r) pair, test validity" is O(n2)O(n^2) and you need to drop to O(n)O(n) amortized.
  • You can describe the window state as a small bag of bookkeeping: a counter, a hash map, a running sum/product, a "missing characters" tally.

Common templates:

Practice Problems (85)

3 Longest Substring Without Repeating Characters
30 Substring with Concatenation of All Words
76 Minimum Window Substring
209 Minimum Size Subarray Sum
395 Longest Substring with At Least K Repeating Characters
424 Longest Repeating Character Replacement
438 Find All Anagrams in a String
567 Permutation in String
713 Subarray Product Less Than K
795 Number of Subarrays with Bounded Maximum
904 Fruit Into Baskets
930 Binary Subarrays With Sum
992 Subarrays with K Different Integers
1004 Max Consecutive Ones III
1040 Moving Stones Until Consecutive II
1156 Swap For Longest Repeated Character Substring
1208 Get Equal Substrings Within Budget
1234 Replace the Substring for Balanced String
1297 Maximum Number of Occurrences of a Substring
1358 Number of Substrings Containing All Three Characters
1493 Longest Subarray of 1's After Deleting One Element
1610 Maximum Number of Visible Points
1658 Minimum Operations to Reduce X to Zero
1695 Maximum Erasure Value
1703 Minimum Adjacent Swaps for K Consecutive Ones
1763 Longest Nice Substring
1800 Maximum Ascending Subarray Sum
1838 Frequency of the Most Frequent Element
1839 Longest Substring Of All Vowels in Order
1888 Minimum Number of Flips to Make the Binary String Alternating
2009 Minimum Number of Operations to Make Array Continuous
2024 Maximize the Confusion of an Exam
2106 Maximum Fruits Harvested After at Most K Steps
2260 Minimum Consecutive Cards to Pick Up
2271 Maximum White Tiles Covered by a Carpet
2302 Count Subarrays With Score Less Than K
2401 Longest Nice Subarray
2411 Smallest Subarrays With Maximum Bitwise OR
2414 Length of the Longest Alphabetical Continuous Substring
2419 Longest Subarray With Maximum Bitwise AND
2444 Count Subarrays With Fixed Bounds
2516 Take K of Each Character From Left and Right
2537 Count the Number of Good Subarrays
2555 Maximize Win From Two Segments
2609 Find the Longest Balanced Substring of a Binary String
2730 Find the Longest Semi-Repetitive Substring
2747 Count Zero Request Servers
2760 Longest Even Odd Subarray With Threshold
2765 Longest Alternating Subarray
2779 Maximum Beauty of an Array After Applying Operation
2781 Length of the Longest Valid Substring
2799 Count Complete Subarrays in an Array
2831 Find the Longest Equal Subarray
2875 Minimum Size Subarray in Infinite Array
2904 Shortest and Lexicographically Smallest Beautiful String
2932 Maximum Strong Pair XOR I
2935 Maximum Strong Pair XOR II
2958 Length of Longest Subarray With at Most K Frequency
2962 Count Subarrays Where Max Element Appears at Least K Times
3013 Divide an Array Into Subarrays With Minimum Cost II
3086 Minimum Moves to Pick K Ones
3090 Maximum Length Substring With Two Occurrences
3095 Shortest Subarray With OR at Least K I
3097 Shortest Subarray With OR at Least K II
3171 Find Subarray With Bitwise OR Closest to K
3208 Alternating Groups II
3234 Count the Number of Substrings With Dominant Ones
3258 Count Substrings That Satisfy K-Constraint I
3261 Count Substrings That Satisfy K-Constraint II
3297 Count Substrings That Can Be Rearranged to Contain a String I
3298 Count Substrings That Can Be Rearranged to Contain a String II
3305 Count of Substrings Containing Every Vowel and K Consonants I
3306 Count of Substrings Containing Every Vowel and K Consonants II
3325 Count Substrings With K-Frequency Characters I
3346 Maximum Frequency of an Element After Performing Operations I
3347 Maximum Frequency of an Element After Performing Operations II
3364 Minimum Positive Sum Subarray
3411 Maximum Subarray With Equal Products
3445 Maximum Difference Between Even and Odd Frequency II
3634 Minimum Removals to Balance Array
3679 Minimum Discards to Balance Inventory
3694 Distinct Points Reachable After Substring Removal
3795 Minimum Subarray Length With Distinct Sum At Least K
3845 Maximum Subarray XOR with Bounded Range
3859 Count Subarrays With K Distinct Integers