Turn a nested loop over every subarray into a single pass, by never recomputing what you already know.
The sliding window is the first pattern worth learning because it converts the most common brute force in interviews, check every subarray, from quadratic to linear. The brute force is O(n²) or O(n²·k). The window is O(n). The gap is the whole question.
Three signals put together tell you it is a window problem.
| Signal | What it looks like in the prompt |
|---|---|
| Contiguity | “subarray”, “substring”, “consecutive”, “in a row”. Not “subsequence”. |
| An extremum | “longest”, “shortest”, “maximum sum”, “minimum length”, “count how many”. |
| A monotone condition | Growing the window can only make the condition harder, shrinking it can only make it easier. Sums of non-negative numbers, distinct-character counts, and “at most k of X” all behave this way. |
target” is a clean window problem when all values are positive. Put one negative number in the array and the window breaks, because total is no longer monotone in the window width. Always ask the interviewer whether values can be negative. Asking earns points; assuming loses them.left and right, marking a half-open run s[left..right]. Keep alongside them a small summary of what is inside: a running sum, a character count, a number of distinct values. Move right forward to grow, move left forward to shrink. Each move updates the summary in O(1). Nothing is ever recomputed from scratch.The brute force recomputes the summary of each candidate run from its own elements, so an element is touched once per window that contains it. The window instead updates the summary incrementally: one addition when an element enters, one subtraction when it leaves.
The inner while loop that shrinks the window looks like it might make the algorithm quadratic. It does not, and the reason is worth being able to state cleanly in an interview.
left and right each only ever increase, and neither can exceed n. So across the whole run there are at most n increments of right and at most n increments of left, giving at most 2n pointer moves in total. Each move does O(1) work. Total: O(n), no matter how the inner loop is distributed.Put another way: every element enters the window exactly once and leaves it at most once. Two touches per element.
Every window problem has one sentence that is true at the top of each iteration. Write it as a comment. It is how you convince yourself, and the interviewer, that the code is right.
window_sum equals the sum of nums[right-k+1 .. right].nums[left..right] is valid, and no window ending at right that starts before left is valid.nums[left..right] is the shortest valid window ending at right, or no valid window ends at right.Nearly every window problem is one of these two. Learn both by heart.
def fixed_window(nums: list[int], k: int) -> int:
"""Best value over every window of exactly k elements."""
window = sum(nums[:k]) # pay for the first window once
best = window
for right in range(k, len(nums)):
window += nums[right] - nums[right - k] # add entering, drop leaving
best = max(best, window)
return best
Use when the prompt names the width. There is no left variable, because left is always right - k + 1.
def variable_window(items: list[int]) -> int:
"""Longest (or shortest) window satisfying some condition."""
left = 0
state = 0 # the running summary of the window
best = 0
for right, value in enumerate(items):
state += value # 1. grow: value enters on the right
while is_invalid(state): # 2. shrink until the window is legal again
state -= items[left]
left += 1
best = max(best, right - left + 1) # 3. record
return best
Three beats: grow, shrink, record. For a longest answer you record after the shrink loop. For a shortest answer you record inside the shrink loop, because each shrink step gives a smaller valid window.
best goes. Ask what the while condition means. If the loop runs while the window is illegal, the window is legal only after the loop, so record after. If the loop runs while the window is legal (you are shrinking a valid window to find the tightest one), record inside.[left, right] inclusive is right - left + 1. Say it out loud every time.left and remove items[left] from the state in the same breath, in that order: remove first, then move.ch in window_list is O(n) and quietly makes the solution quadratic. Use a set or a Counter.sum(nums[left:right+1]) throws away the whole point of the pattern.math.inf) and a final check that a valid window was ever found.Given an array of integers nums and an integer k, return the maximum sum of any contiguous subarray of length exactly k.
n - k + 1 windows from scratch: O(n·k).def max_subarray_sum_of_size_k(nums: list[int], k: int) -> int:
"""Return the largest sum of any contiguous subarray of length k.
Args:
nums: Input numbers. Values may be negative.
k: Window length. Must satisfy 1 <= k <= len(nums).
Returns:
The maximum window sum.
Raises:
ValueError: If k is outside 1..len(nums).
Example:
>>> max_subarray_sum_of_size_k([2, 1, 5, 1, 3, 2], 3)
9
"""
if not 1 <= k <= len(nums):
raise ValueError(f"k must be in 1..{len(nums)}, got {k}")
# Invariant: window_sum == sum(nums[right - k + 1 : right + 1])
window_sum = sum(nums[:k])
best = window_sum
for right in range(k, len(nums)):
window_sum += nums[right] - nums[right - k]
best = max(best, window_sum)
return best
With nums = [2, 1, 5, 1, 3, 2] and k = 3:
| right | enters | leaves | window_sum | best |
|---|---|---|---|---|
| — | 2, 1, 5 | — | 8 | 8 |
| 3 | 1 | 2 | 7 | 8 |
| 4 | 3 | 1 | 9 | 9 |
| 5 | 2 | 5 | 6 | 9 |
k == len(nums): one window, the loop body never runs, the initial sum is the answer.k > len(nums): undefined. Raise rather than silently return a wrong number.best starts at the real first window, not at 0. Starting best = 0 is the classic bug here.k - 1 elements, so I only need the difference between them, which makes each step constant time.”Given a string s, return the length of the longest substring with no repeated characters. For "abcabcbb" the answer is 3, from "abc".
ch is already inside the window, the window is illegal. Every window starting at or before the previous occurrence of ch is also illegal, so jump left straight past it.def length_of_longest_substring(s: str) -> int:
"""Length of the longest substring of s with all-distinct characters.
Args:
s: Input string. May be empty.
Returns:
The length of the longest run with no repeats. 0 for an empty string.
Example:
>>> length_of_longest_substring("pwwkew")
3
"""
last_seen: dict[str, int] = {} # character -> index of its last occurrence
left = 0 # first index still inside the window
best = 0
for right, ch in enumerate(s):
# Only a repeat *inside* the current window forces a jump.
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
>= left check mattersThe dictionary remembers characters that have already fallen out of the window. Take "abba". At right = 3 the character 'a' was last seen at index 0, but left is already 2, so 'a' is not in the window and no jump is needed. Without the guard, left would move backwards to 1 and the answer would be wrong. This one comparison is what the interviewer is watching for.
s = "abba":
| right | ch | jump? | left | window | best |
|---|---|---|---|---|---|
| 0 | a | no | 0 | "a" | 1 |
| 1 | b | no | 0 | "ab" | 2 |
| 2 | b | yes, to 2 | 2 | "b" | 2 |
| 3 | a | no, 0 < left | 2 | "ba" | 2 |
0; the loop never runs.1.left past the previous occurrence, because every start before that point is already invalid.”Given an array of positive integers nums and a positive integer target, return the minimal length of a contiguous subarray whose sum is at least target. Return 0 if no such subarray exists.
target, the window is valid. Now shrink from the left as far as it will go while staying valid, recording the length at each step. That finds the shortest valid window ending at this right.best is updated inside the shrink loop.import math
def min_subarray_len(target: int, nums: list[int]) -> int:
"""Shortest contiguous subarray of nums with sum >= target.
Assumes every value in nums is positive, which is what makes the
running total monotone in the window width and the shrink step valid.
Args:
target: Required minimum sum, positive.
nums: Positive integers.
Returns:
The length of the shortest qualifying subarray, or 0 if there is none.
Example:
>>> min_subarray_len(7, [2, 3, 1, 2, 4, 3])
2
"""
left = 0
total = 0
best = math.inf
for right, value in enumerate(nums):
total += value
# The window is valid; squeeze it from the left while it stays valid.
while total >= target:
best = min(best, right - left + 1)
total -= nums[left]
left += 1
return 0 if best == math.inf else int(best)
target = 7, nums = [2, 3, 1, 2, 4, 3]:
| right | total after add | shrink steps | best |
|---|---|---|---|
| 0..2 | 2, 5, 6 | none, below target | ∞ |
| 3 | 8 | drop 2 → 6, left=1 | 4 |
| 4 | 10 | drop 3 → 7, drop 1 → 6, left=3 | 3 then 2 |
| 5 | 9 | drop 2 → 7, drop 4 → 3, left=5 | 2 |
The answer is 2, from [4, 3].
target: best stays inf, return 0.target: the shrink loop finds length 1.0.Given strings s and t, return the shortest substring of s that contains every character of t, counting repeats. Return "" if there is none. For s = "ADOBECODEBANC" and t = "ABC" the answer is "BANC".
need is a counter that starts as the counts in t. As characters enter the window it is decremented, so a value can go negative, meaning the window holds a surplus of that character.missing tracks how many required characters, counting repeats, are still unmet. The window is valid exactly when missing == 0. This is what keeps validity a O(1) test rather than a dictionary comparison.import math
from collections import Counter
def min_window(s: str, t: str) -> str:
"""Shortest substring of s containing every character of t, with repeats.
Args:
s: The string to search.
t: The required characters, with multiplicity.
Returns:
The shortest qualifying substring, or "" if none exists.
Example:
>>> min_window("ADOBECODEBANC", "ABC")
'BANC'
"""
if not s or not t or len(t) > len(s):
return ""
need: Counter[str] = Counter(t) # >0 means still owed, <0 means surplus
missing = len(t) # required characters not yet covered
best_start, best_len = 0, math.inf
left = 0
for right, ch in enumerate(s):
# Grow. A positive need means this character actually pays down a debt.
if need[ch] > 0:
missing -= 1
need[ch] -= 1
# Valid window. Shrink from the left while it stays valid.
while missing == 0:
if right - left + 1 < best_len:
best_start, best_len = left, right - left + 1
leaving = s[left]
need[leaving] += 1
if need[leaving] > 0: # we just gave back a character we needed
missing += 1
left += 1
return "" if best_len == math.inf else s[best_start : best_start + best_len]
This is the subtle part, and the reason the code is short. Suppose t = "AB" and the window is "AAB". Then need['A'] == -1. When the leftmost 'A' leaves, need['A'] becomes 0, which is not positive, so missing stays at 0 and the window is still valid, correctly. Only when a genuinely needed character leaves does need rise above zero and missing tick back up. The sign of the counter encodes surplus versus debt for free.
s = "ADOBECODEBANC", t = "ABC". The window first becomes valid at "ADOBEC", length 6. Shrinking is blocked immediately, because dropping 'A' breaks it. Later the window "CODEBA" is valid at length 6, then "BANC" at length 4, which is the answer.
| First valid window | Length | best |
|---|---|---|
| ADOBEC | 6 | ADOBEC |
| CODEBA | 6 | ADOBEC |
| BANC | 4 | BANC |
need also picks up entries for characters of s that are not in t, so strictly it is O(|s| + |t|) keys in the worst case. Guarding with if ch in need before touching the counter keeps it at O(|t|) and is worth a sentence if the interviewer pushes on memory.t longer than s: the early return handles it.t has repeats, such as t = "AABC": handled, because missing counts multiplicity rather than distinct characters.s == t: the whole string is the answer.best_len stays inf and the function returns "".missing, so checking whether the window is valid is O(1) instead of comparing two dictionaries. The counter goes negative to record surplus characters, which is what makes the shrink step correct.”left. Variable width uses Template B: grow, shrink, record.n times, so at most 2n moves in total.Pattern 2, Two Pointers, keeps the two-index idea but changes the motion: instead of both pointers walking forward, they start at opposite ends and converge. That small change unlocks sorted-array problems the window cannot touch.