4 — Sliding Window
“Find the maximum sum of any 3 consecutive elements” has an obvious brute force: for every starting position, sum the next 3 elements, keep the best. It works, and it re-does almost all of its own work — the sum for position 5 shares two of its three elements with the sum for position 4, and the brute force throws that overlap away and recomputes it from nothing. Sliding window is the observation that you never need to: keep a running window over the data and slide it, updating the running answer with only the element entering and the element leaving, instead of re-scanning the whole window from scratch. Same answer, one pass.
The Core Idea: A Window That Slides Instead of Restarting
A window is a contiguous slice of the array or string, tracked by two indices — left and
right — that both move forward, never backward. Brute force fixes left, walks right through
the remaining elements, advances left, and repeats: nested loops, O(n·k) for a fixed window of
size k or O(n²) for a variable one. Sliding window keeps a running aggregate — a sum, a count, a
hash set of the window’s contents — and pays only for the change between one window and the next,
turning that nested loop into a single pass.
Every sliding-window problem reduces to one decision: fixed size (slide by one, one element in,
one out) or variable size (grow right to explore, shrink left only when forced)? The rest of
this chapter works both, plus the detail that causes most bugs: two different reasons a variable
window shrinks, depending on whether you want the longest valid window or the shortest one.
Fixed-Size Window: Maximum Sum Subarray
Problem: given an array and an integer k, find the maximum sum of any contiguous subarray of size k.
Brute force sums k elements at every one of the n−k+1 starting positions — O(n·k). The fix: sum the first window once, then slide right one position at a time. Each slide adds one incoming element and subtracts one outgoing element — the rest of the window is untouched, so there’s nothing to re-sum.
def max_sum_subarray(nums: list[int], k: int) -> int:
if len(nums) < k:
raise ValueError("array shorter than window size")
window_sum = sum(nums[:k])
best = window_sum
for right in range(k, len(nums)):
left = right - k
window_sum += nums[right] - nums[left] # one in, one out
best = max(best, window_sum)
return best
# max_sum_subarray([2, 1, 5, 1, 3, 2], 3) == 9 ([5, 1, 3])
Complexity: O(n) time — the initial sum is O(k), then n−k slides at O(1) each. O(1) space — one running total, no auxiliary structure. The window never shrinks below k or grows past it; it just translates rightward one index at a time, the simplest member of the family and the one to reach for whenever the problem hands you a fixed k up front.
Variable-Size Window: Longest Substring Without Repeating Characters
Problem: given a string, find the length of the longest substring with no repeated characters.
Here the window size isn’t given — it’s exactly what you’re solving for. The general template for every variable-size window problem:
expand right by one → always
check the constraint → after every expansion
shrink left while violated → repeat until the window is valid again
record the answer → once the window is valid
The constraint here is “no duplicate characters in the window,” tracked with a hash set of the
characters currently inside [left, right]. Expanding right is unconditional; a duplicate found
on expansion is resolved by shrinking left one step at a time — removing each evicted character
from the set — until the duplicate is gone and the window is valid again.
def longest_unique_substring(s: str) -> int:
window: set[str] = set()
left = 0
best = 0
for right in range(len(s)):
# shrink while the incoming character violates the constraint
while s[right] in window:
window.remove(s[left])
left += 1
window.add(s[right])
best = max(best, right - left + 1) # record after the window is valid
return best
# longest_unique_substring("abcabcbb") == 3 ("abc")
# longest_unique_substring("pwwkew") == 3 ("wke")
Complexity: O(n) time — right advances n times, left advances at most n times total across
the whole run, not per iteration of right (why that’s true is its own section, below). O(min(n,
alphabet size)) space for the set. The shrink condition: keep shrinking while the window is
invalid, stop the moment it’s valid again. That direction is about to flip.
Variable-Size Window, Inverted: Minimum Window Substring
Problem: given a string s and a string t, find the smallest substring of s that contains
every character of t (with at least its multiplicity — two 'a's in t needs at least two 'a's
in the window).
This reads like the same template with “longest” swapped for “shortest,” and that swap is exactly where the bug lives if you’re not careful. In the previous problem the window shrinks because it became invalid and stops the moment it’s valid again. Here it’s the opposite: the window shrinks because it’s valid — shrinking is how you look for something smaller than what you already have, and you keep doing it until shrinking would make it invalid again.
longest-valid-window shrink: while INVALID: shrink (stop as soon as valid)
shortest-valid-window shrink: while VALID: shrink, record (stop as soon as invalid)
A hash map tracks how many of each required character the window still needs; a counter tracks how many distinct required characters are currently satisfied. The window becomes a candidate answer only once every required character is satisfied — and from there, shrinking from the left is how you find out whether an even smaller valid window exists.
from collections import Counter
def min_window_substring(s: str, t: str) -> str:
if not t or not s:
return ""
need = Counter(t)
missing = len(need) # count of distinct chars not yet satisfied
left = 0
best_len = float("inf")
best_start = 0
for right, char in enumerate(s):
if char in need:
need[char] -= 1
if need[char] == 0:
missing -= 1 # this character is now fully satisfied
# window is valid (all required chars satisfied) — shrink to look for smaller
while missing == 0:
if right - left + 1 < best_len:
best_len = right - left + 1
best_start = left
left_char = s[left]
if left_char in need:
need[left_char] += 1
if need[left_char] > 0:
missing += 1 # shrinking broke validity — stop after this step
left += 1
return "" if best_len == float("inf") else s[best_start:best_start + best_len]
# min_window_substring("ADOBECODEBANC", "ABC") == "BANC"
Complexity: O(n) time, O(k) space where k is the alphabet size of t (bounded by the need
map). Same two-pointer skeleton as the previous problem, opposite shrink trigger — the whole reason
to hold both examples side by side rather than learning either one in isolation.
Recognizing Which Flavor You Need
The problem statement almost always signals which variant is wanted, if you read for these cues:
| Phrasing in the problem | Flavor |
|---|---|
| ”of size k” / “of length k” given up front | Fixed-size window |
| ”longest substring/subarray that…” | Variable, shrink while invalid |
| ”shortest/minimum substring/subarray that…” | Variable, shrink while valid |
| ”at most k distinct” / “no more than k violations” | Variable, shrink while invalid |
| ”contains all of…” / “at least k occurrences of…” | Variable, shrink while valid |
The one-line test that resolves ambiguity: what are you recording the answer against? Recording only once the window is valid again, right after shrinking past invalidity is the longest-flavor shrink. Recording while the window is still valid, shrinking further to see if something smaller survives is the shortest-flavor shrink. Mixing the two up produces code that compiles, runs, and returns a confidently wrong answer on the second test case — a silent failure, which is exactly why it’s worth pausing on before writing the shrink loop rather than after debugging it.
Why This Is O(n), Not O(n²)
The code has a for loop with a while loop nested inside it, and nested loops read as O(n²) on
reflex — the reflex is wrong here, and it’s worth being able to say precisely why, not just cite the
conclusion.
right is the outer loop’s index: it advances exactly once per outer iteration, n times total,
never backward. left is the inner loop’s index: it also only ever advances, never resets to 0 —
across the entire function call, not per outer iteration, left can advance at most n times
total, because it’s bounded above by right and both are monotonically non-decreasing over an array
of length n.
That’s the amortized argument from 2 — Asymptotic Analysis (Part 01, Chapter 2) applied directly:
don’t ask “how much work does the inner while do on one specific outer iteration” — some
iterations do nothing, others shrink several steps at once, and bounding the worst single iteration
would overcount. Ask instead “how much total work does the inner loop do summed across every outer
iteration” — bounded by n, because left only has n positions to ever visit, full stop, regardless
of how those visits are distributed. Two pointers, each visiting each index a bounded number of
times, sums to O(n) + O(n) = O(n) total — not O(n) per outer step, which is the shape that would
actually earn an O(n²). 3 — Two Pointers leans on the same reasoning for the opposite-direction
techniques — sliding window is the identical amortized argument, applied to a window instead of a
pair of converging ends.
Metadata
| Author | Amit Singh |
| Scope | data-structures-algorithms |
Local graph
Linked from 5 notes
6 — Monotonic Queue
Sliding window maximum in O(n): a deque of indices kept decreasing by value, trimmed from the back for domination and from the front for window expiry — the second eviction rule a monotonic stack structurally can't support.
3 — Two Pointers
Opposite-direction and same-direction pointer techniques for sorted arrays — Two Sum II, Container With Most Water, 3Sum, and in-place duplicate removal — plus how to tell the pattern apart from sliding window.
2 — Queue
FIFO fundamentals: enqueue/dequeue, why list.pop(0) is a silent O(n) trap, collections.deque as the fix, the two-stack amortized-O(1) implementation, and BFS as the queue's signature use case.
5 — Monotonic Stack
A stack that enforces increasing or decreasing order at push time, solving next-greater-element, daily-temperatures, and largest-rectangle-style problems in O(n).
Data Structures & Algorithms
A book-shaped table of contents for MAANG-interview DSA prep: Python language foundations, mathematical and algorithmic foundations, arrays/strings, linked structures, stacks/queues, trees, graphs, sorting/searching, dynamic programming, greedy algorithms, backtracking, bit manipulation, advanced data structures, advanced algorithms, interview problem patterns, and MAANG interview mastery — a book-length progression from fundamentals to Google/Meta/Amazon/Apple/Netflix/Microsoft (L4–L6) interview readiness.
Related notes
5 — Prefix Sum & Difference Arrays
Precomputed running sums and difference arrays for O(1) range-sum queries and range-update problems.
6 — Hashing
How Python's dict/set turn an O(n) or O(n²) scan into O(1) average-case lookups, when that average case breaks down, and where hashing trades away information — order — that a problem still needs.
7 — Strings
Why treating a Python string as 'just an array of characters' is only half true — immutability turns naive concatenation quietly quadratic, and that one property shapes every string algorithm that follows.
8 — String Algorithms
Palindrome checks via two pointers and expand-around-center, anagram detection by counting vs. sorting, the naive O(n·m) substring search baseline, and why Python's string immutability turns 'reverse in place' into a trick question.