Filter concepts by levelShowing all levels.

Data Structures & Algorithms · Section 7

Sliding Window

Level
beginner
Read
70 min
Concepts
7

The two window shapes — fixed-size (subtract the outgoing, add the incoming) and variable-size (grow with right, shrink with left) — how their state is kept incremental instead of resummed, three canonical string problems built on them, and the contiguous-subarray/substring signal that tells you to reach for this pattern.

What is true here

  1. A fixed-size window updates in O(1) per step by removing the outgoing element and adding the incoming one — never resumming the whole window.
  2. A variable-size window grows with right and shrinks with left using a while loop, since a window can often shrink more than once per extension.
  3. Longest substring without repeating characters uses a last-seen-index map so the left edge can jump directly past a duplicate.
  4. Contiguous plus a sum/length/composition condition is the standard signal that a problem wants a sliding window.

What you will be able to do

  • Compute a fixed-size window aggregate (sum, count) in O(n) instead of O(n × k)
  • Grow and shrink a variable-size window to find the smallest or longest substring/subarray satisfying a condition
  • Solve the longest-substring-without-repeats and minimum-window-substring problems from the underlying pattern, not from memory
  • Recognize a contiguous subarray/substring problem as sliding-window before reaching for anything heavier

Concepts

The two window shapes, how their state stays incremental, and the classic problems built on top of them.

Fixed-size window (e.g. max sum of size-k subarray)

corebeginner

A fixed-size window of exactly k elements slides across an array one step at a time — only the element leaving and the element entering change the running total, so nothing needs to be resummed.

Think of it as

Picture a k-wide frame sliding across the array. Build the total for the first k elements once. Each time the frame slides one step right, subtract the element that just left it and add the element that just entered — the new total is always one subtraction and one addition away from the last one.

What we're doing: Find the maximum sum of any contiguous size-k subarray.

max_sum_fixed_window.pypython
def max_sum_fixed_window(nums, k):
    window_sum = sum(nums[:k])
    best = window_sum
    for i in range(k, len(nums)):
        window_sum += nums[i] - nums[i - k]
        best = max(best, window_sum)
    return best


print(max_sum_fixed_window([2, 1, 5, 1, 3, 2], 3))
2
The first window is built once, the ordinary way — there is nothing to slide from yet.
4
nums[i] just entered the window; nums[i - k] just left it — exactly k elements are in the window at every step.
Output
9

Why this works: The window [1, 5, 1] → sum 7 slides to [5, 1, 3] → sum 9 by removing the leading 1 and adding the trailing 3 — no resumming. 9 turns out to be the best of all four size-3 windows.

Recomputing sum(nums[i:i+k]) at every window position

Wrong

python
def max_sum_fixed_window_slow(nums, k):
    best = float("-inf")
    for i in range(len(nums) - k + 1):
        window_sum = sum(nums[i:i + k])
        best = max(best, window_sum)
    return best


print(max_sum_fixed_window_slow([2, 1, 5, 1, 3, 2], 3))

Better

python
def max_sum_fixed_window(nums, k):
    window_sum = sum(nums[:k])
    best = window_sum
    for i in range(k, len(nums)):
        window_sum += nums[i] - nums[i - k]
        best = max(best, window_sum)
    return best

What you see: max_sum_fixed_window_slow gives the same correct answer (9) here, then times out once n and k both grow large (e.g. n ~ 10⁵, k ~ 10⁴).

Why: sum(nums[i:i+k]) resums all k elements from scratch at every one of the n - k + 1 positions — O(n × k) total. Updating the running total by one subtraction and one addition per step drops that to O(n).

  • Exactly the example above — track the running sum and divide by k at the end.

  • Sliding Window MaximumhardLeetCode

    The window is still fixed-size, but the state (the running maximum) is not a simple running total — a monotonic deque is needed to update it in O(1) amortized per step.

Remember: A fixed window updates by subtracting the outgoing element and adding the incoming one — resumming the whole window at every position is the O(n × k) trap this pattern exists to avoid.

Was this helpful?

Variable-size window that grows/shrinks based on a condition

corebeginner

A variable-size window grows by moving its right edge forward, and shrinks by moving its left edge forward whenever a condition demands it — the width adapts to the data instead of staying fixed.

Think of it as

right always moves forward, one step at a time, folding the new element into the window's running state. After each extension, check the condition: if the window has become invalid (or has satisfied the goal), move left forward — shrinking the window and removing its state — until the window is valid (or the answer at this width has been recorded) again.

What we're doing: Find the length of the smallest contiguous subarray whose sum is at least a target.

min_subarray_len.pypython
def min_subarray_len(nums, target):
    left = 0
    window_sum = 0
    best = float("inf")
    for right in range(len(nums)):
        window_sum += nums[right]
        while window_sum >= target:
            best = min(best, right - left + 1)
            window_sum -= nums[left]
            left += 1
    return best if best != float("inf") else 0


print(min_subarray_len([2, 3, 1, 2, 4, 3], 7))
6
right always extends the window, adding nums[right] to the running sum.
7
while, not if — the window keeps shrinking as long as it is still valid, since a smaller valid window is always at least as good.
9
Each shrink undoes exactly the sum contribution of the element leaving on the left.
Output
2

Why this works: The window grows to [2, 3, 1, 2] (sum 8, width 4) before it first reaches the target, then keeps shrinking and re-extending as right advances — the smallest window found along the way is [4, 3] (sum 7, width 2).

Shrinking with `if` instead of `while`

Wrong

python
def min_subarray_len_wrong(nums, target):
    left = 0
    window_sum = 0
    best = float("inf")
    for right in range(len(nums)):
        window_sum += nums[right]
        if window_sum >= target:
            best = min(best, right - left + 1)
            window_sum -= nums[left]
            left += 1
    return best if best != float("inf") else 0


print(min_subarray_len_wrong([2, 3, 1, 2, 4, 3], 7))

Better

python
def min_subarray_len(nums, target):
    left = 0
    window_sum = 0
    best = float("inf")
    for right in range(len(nums)):
        window_sum += nums[right]
        while window_sum >= target:
            best = min(best, right - left + 1)
            window_sum -= nums[left]
            left += 1
    return best if best != float("inf") else 0

What you see: min_subarray_len_wrong([2, 3, 1, 2, 4, 3], 7) returns 4 — the correct answer is 2.

Why: An if only shrinks the window ONE step per right-advance, even when it could still shrink further and stay valid. The while keeps removing from the left as long as the window remains valid, which is what actually finds the smallest one — stopping after a single shrink leaves a wider window than necessary on the table.

  • Contains Duplicate IIeasyLeetCode

    A window that must never exceed size k — shrink from the left the moment it does, checking membership as you go.

  • Minimum Size Subarray SummediumLeetCode

    Exactly the example above — grow with right, shrink with a while loop on left.

Remember: Shrink with a while, not an if — a window that is still valid after one shrink might stay valid after several more, and only the while keeps checking.

Was this helpful?

Window state maintained incrementally (not recomputed each shift)

standardbeginner

A sliding window's state — a running sum, a frequency map, a count of distinct values — should be updated by exactly what changed as the window moves, never rebuilt from scratch at each position.

Think of it as

Every value entering the window updates the state by exactly its own contribution (add to a sum, increment a count); every value leaving undoes that exact same update (subtract from the sum, decrement the count, delete the key at zero). Because each element does this once on entry and once on exit, the total update work across the whole scan is proportional to n, not to n times the window size.

What we're doing: Maintain a running character-frequency map for a fixed-size window as it slides, updating only what changed.

window_char_counts.pypython
from collections import defaultdict


def window_char_counts(s, k):
    counts = defaultdict(int)
    for ch in s[:k]:
        counts[ch] += 1
    snapshots = [dict(counts)]
    for i in range(k, len(s)):
        counts[s[i]] += 1
        counts[s[i - k]] -= 1
        if counts[s[i - k]] == 0:
            del counts[s[i - k]]
        snapshots.append(dict(counts))
    return snapshots


for snap in window_char_counts("aabbcc", 3):
    print(snap)
6
Only the first window is built the ordinary way — one pass over its k characters.
8
Sliding by one only ever touches the two characters that changed: the one entering and the one leaving.
Output
{'a': 2, 'b': 1}
{'a': 1, 'b': 2}
{'b': 2, 'c': 1}
{'b': 1, 'c': 2}

Why this works: The window starts at "aab" ({'a': 2, 'b': 1}). Sliding to "abb" only updates 'a' (leaving, 2→1) and 'b' (entering, 1→2). Sliding again to "bbc" fully removes 'a' the moment its count hits 0, rather than leaving a stale zero entry behind.

Remember: A window state is updated by what entered and what left, never rebuilt — and a count must be deleted at zero, not left sitting there, or a distinct-count check like len(counts) will lie.

Was this helpful?

Longest substring without repeating characters

corebeginner

The longest run of a string with no repeated character comes from a variable-size window that jumps its left edge past a duplicate the instant one appears, tracking the widest duplicate-free window seen.

Think of it as

Track the LAST INDEX each character was seen at. Extend right one character at a time. If the current character was already seen at or after left (meaning it is inside the current window), jump left directly to one past that earlier occurrence — no need to shrink one character at a time. Either way, the window from left to right is now guaranteed duplicate-free, so its width is a candidate for the longest answer.

What we're doing: Find the length of the longest substring with no repeated character.

longest_no_repeat.pypython
def length_of_longest_substring(s):
    last_seen = {}
    left = 0
    best = 0
    for right, ch in enumerate(s):
        if ch in last_seen and last_seen[ch] >= left:
            left = last_seen[ch] + 1
        last_seen[ch] = right
        best = max(best, right - left + 1)
    return best


print(length_of_longest_substring("abcabcbb"))
print(length_of_longest_substring("bbbbb"))
5
last_seen[ch] >= left confirms the earlier occurrence is still inside the current window — an old occurrence before left is irrelevant.
6
Jumping directly to one past the duplicate skips every character in between in a single step, instead of removing them one at a time.
Output
3
1

Why this works: "abcabcbb" has its longest duplicate-free run as "abc" (length 3) — every attempt to extend further immediately re-hits an already-seen character. "bbbbb" repeats "b" at every step, so the window can never grow past length 1.

Jumping left on any earlier occurrence, without checking it is still inside the window

Wrong

python
def length_of_longest_substring_wrong(s):
    last_seen = {}
    left = 0
    best = 0
    for right, ch in enumerate(s):
        if ch in last_seen:
            left = last_seen[ch] + 1
        last_seen[ch] = right
        best = max(best, right - left + 1)
    return best


print(length_of_longest_substring_wrong("abba"))

Better

python
def length_of_longest_substring(s):
    last_seen = {}
    left = 0
    best = 0
    for right, ch in enumerate(s):
        if ch in last_seen and last_seen[ch] >= left:
            left = last_seen[ch] + 1
        last_seen[ch] = right
        best = max(best, right - left + 1)
    return best

What you see: length_of_longest_substring_wrong("abba") returns 3, but the true longest duplicate-free substring in "abba" has length 2 ("ab" or "ba").

Why: By the time right reaches the second 'a' (index 3), left has already moved to 2 (after the repeated 'b'). But last_seen['a'] is still 0, from before the window started — jumping left back to 1 without checking last_seen[ch] >= left moves left BACKWARD, letting the window count characters that were already shrunk past.

  • Contains DuplicateeasyLeetCode

    The yes/no version of the same seen-before idea, with no window at all — worth confirming first.

  • Exactly the example above — a last-seen-index map lets left jump directly past a duplicate.

Remember: Only jump left past a duplicate whose last-seen index is still inside the window (>= left) — an occurrence from before the window started must not move left backward.

Was this helpful?

Minimum window substring

standardintermediate

The smallest window in one string that contains every character of another (with repeats) is found by growing the window until it qualifies, then shrinking it from the left as far as possible while it still does.

Think of it as

Count how many of each required character are still needed, plus one running total of how many required character-slots overall remain unfilled. Extending right fills a slot whenever the character taken is still needed. The moment every slot is filled, the window is valid — shrink from the left as long as it stays valid, recording the smallest valid window seen, before continuing to extend.

What we're doing: Find the smallest substring of s that contains every character of t, including repeats.

min_window.pypython
from collections import Counter


def min_window(s, t):
    if not t or not s:
        return ""
    need = Counter(t)
    missing = len(t)
    left = 0
    best_left, best_right = 0, 0
    for right, ch in enumerate(s, 1):
        if need[ch] > 0:
            missing -= 1
        need[ch] -= 1
        if missing == 0:
            while need[s[left]] < 0:
                need[s[left]] += 1
                left += 1
            if best_right == 0 or right - left < best_right - best_left:
                best_left, best_right = left, right
            need[s[left]] += 1
            missing += 1
            left += 1
    return s[best_left:best_right]


print(min_window("ADOBECODEBANC", "ABC"))
8
missing counts required slots, not distinct characters — two required "A"s are two slots, both must be filled.
9
need[ch] going negative just means "more of this character than required is currently in the window" — that is fine, not an error.
10
missing == 0 means every required slot is currently filled somewhere in the window — the window just became valid.
12
Shrinking only pulls in characters the window has MORE than enough of (need[s[left]] < 0) — pulling in a still-needed character would break validity.
Output
BANC

Why this works: "BANC" is the shortest substring of "ADOBECODEBANC" containing at least one A, one B, and one C — every shorter window either misses one of the three required characters or is not contiguous.

  • Ransom NoteeasyLeetCode

    The required-count half of this pattern in isolation, with no window at all — Counter(magazine) needs to cover Counter(ransomNote) key by key.

  • Exactly the example above — a required-count map plus one "slots remaining" counter.

Remember: Track how many required slots remain unfilled with one counter, and only shrink past characters the window has a surplus of — pulling in a still-needed one breaks validity.

Was this helpful?

Longest substring with at most K distinct characters

standardbeginner

The longest substring with at most k distinct characters grows the window while a frequency map holds k or fewer keys, and shrinks from the left the moment a (k+1)th distinct character shows up.

Think of it as

Keep a frequency map of the characters currently in the window. Every new character on the right is added to the map. If the map now holds more than k distinct keys, shrink from the left — decrementing counts and deleting a key entirely once it reaches zero — until the map is back down to k keys or fewer. The window is guaranteed valid again after that shrink, so its width is a candidate answer.

What we're doing: Find the length of the longest substring containing at most k distinct characters.

longest_k_distinct.pypython
def longest_substring_k_distinct(s, k):
    counts = {}
    left = 0
    best = 0
    for right, ch in enumerate(s):
        counts[ch] = counts.get(ch, 0) + 1
        while len(counts) > k:
            left_ch = s[left]
            counts[left_ch] -= 1
            if counts[left_ch] == 0:
                del counts[left_ch]
            left += 1
        best = max(best, right - left + 1)
    return best


print(longest_substring_k_distinct("eceba", 2))
6
len(counts) is only accurate here because a key is deleted at count 0, not left sitting there.
9
The shrink loop runs until len(counts) is back within the limit, which for a single new character is at most one shrink.
Output
3

Why this works: "ece" is the longest window in "eceba" using at most 2 distinct characters ('e' and 'c') — extending to include 'b' would introduce a third distinct character and force a shrink.

Remember: Grow the window freely, but shrink the moment the frequency map holds more than k distinct keys — and always delete a key at count 0, or the distinct-key count itself becomes wrong.

Was this helpful?

Recognizing "contiguous subarray/substring satisfying X" as a sliding-window signal

referencebeginner

Whenever a problem asks about a CONTIGUOUS subarray or substring — not any subset, specifically an unbroken run — that satisfies a condition on its sum, length, or character composition, try a sliding window before anything heavier.

Remember: Contiguous plus a condition on sum, length, or composition is the sliding-window signal — a fixed size means a fixed window, an open-ended condition means a variable one.

Was this helpful?

Spotted a mistake on this page? Tell us.