Search Tech Journey

Find topics, journeys and posts

back to blog
algorithmsintermediate 60m read

L09 · Sliding Window I — Fixed Size

Maintaining a window aggregate incrementally so that sliding costs O(1) instead of O(k), which turns the obvious O(n*k) scan into a single pass.

🧩DSAPhase 1 · Core patterns· Session 009 of 130 60 min

🎯 Own the fixed-size sliding window: add the entering element, drop the leaving element, and keep a running aggregate so every slide is O(1).

Series: LeetCode — From Basics to Interview-Ready · Session 9 / 65 · Phase 1 · Core patterns

Watch first

What this pattern IS

Many problems ask about every contiguous block of exactly k elements: the max sum of any k consecutive numbers, the average of every window of k, whether any window of length k is an anagram of a target.

The obvious approach: for each starting index, sum up the k elements. That's n starting positions times k work each = O(n·k). For a million-element array and k = 1000, that's a billion operations — too slow.

The insight: consecutive windows overlap in all but two elements. Window [i, i+k) and window [i+1, i+k+1) share k−1 elements. So instead of recomputing the whole sum, add the one element that entered on the right and subtract the one that left on the left. That's O(1) per slide, O(n) total.

The naive scan throws away work; the sliding window reuses it.

A bus of fixed capacity
🌍 Real world
A bus holds exactly k passengers. At each stop one new passenger boards at the back and, to stay at capacity, one leaves from the front. You don't recount everyone — you adjust by +1 in and −1 out.
💻 Code world
total += nums[right]; total -= nums[right - k] — one element enters, one leaves, count stays k.

First principles

From first principles
Start with the question
Why can a window aggregate be maintained in O(1) per slide instead of O(k)?
  1. 1
    Adjacent windows of size k differ in exactly two elements.
    forced by · Sliding right by one drops the leftmost old element and adds one new rightmost element; the middle k−1 are unchanged.
  2. 2
    A sum (or count) is a decomposable aggregate: new = old − leaving + entering.
    forced by · Addition has an inverse (subtraction), so you can undo the departing element's contribution exactly.
  3. 3
    Therefore each slide costs O(1), and there are n − k + 1 slides.
    forced by · The whole array is traversed once with constant work per step.
  4. 4
    Not every aggregate is O(1)-updatable this way (e.g. maximum).
    forced by · Removing an element from a max has no cheap inverse — you may need to rescan. That needs a monotonic deque (L18), not a plain counter.
⇒ Therefore
Fixed-size window = incremental aggregate. Works cleanly for sum/count; needs extra structure for min/max.

The templates

Template A — running numeric aggregate (sum, then derive average)

def fixed_window_best_sum(nums, k):
    """Max sum of any k consecutive elements."""
    window = sum(nums[:k])          # build the first window explicitly, O(k)
    best = window
    for right in range(k, len(nums)):   # right = index of the ENTERING element
        window += nums[right]           # add the entering element
        window -= nums[right - k]       # drop the element that just left
        best = max(best, window)
    return best

Two invariants: after processing index right, window holds the sum of exactly the k elements ending at right; and right - k is always the index that just fell out of the window.

Template B — fixed window over a frequency map

from collections import Counter
 
def fixed_window_counts(s, k):
    """Slide a size-k window and keep a live Counter of its characters."""
    win = Counter(s[:k])            # first window's letter counts
    # ... check win here for the first window ...
    for right in range(k, len(s)):
        win[s[right]] += 1          # entering char
        left = s[right - k]         # leaving char
        win[left] -= 1
        if win[left] == 0:
            del win[left]           # keep the map clean so len(win) means "distinct in window"
        # ... check win here ...
    return None

Deleting zero-count keys is the detail people forget; it keeps len(win) equal to the number of distinct characters actually in the window.

Mental model

Mental modelAdd one, drop one
A rigid frame of width k slides right. Each step: +entering (index right), −leaving (index right − k). The frame never changes size. Check the answer after each adjustment.
  • Build the first window in O(k), then slide.
  • right is the entering index; right − k is the leaving index.
  • Sum/count update in O(1); min/max need a deque.
  • Keep count maps clean — delete keys that hit zero.
🔔 Fires when you see
'every window of size k', 'consecutive k elements', 'subarray of length exactly k', or 'fixed-length anagram/substring'.

Memory / mnemonic

The whole pattern is two arithmetic lines. Peg them with "IN then OUT, then check."

  • IN = window += nums[right] — the new element boards.
  • OUT = window -= nums[right - k] — the oldest element leaves.
  • CHECK = update your answer.

Say the index rhyme: "right enters, right-minus-k exits." If you ever write right - k + 1 or left = 0, you've broken the frame — the leaving index is always exactly k behind the entering one.

For the count variant, add a fourth beat: "IN, OUT, CLEAN, check" — CLEAN = delete the zero-count key.

The trap

Common misconception
✗ What most people think
For a fixed window you should recompute the aggregate over the whole window on each step to be safe.
✓ What is actually true
That reintroduces the O(n·k) cost you were trying to kill. The entire point is the O(1) incremental update: add entering, subtract leaving.
Why the myth is so sticky
Adjacent windows share k−1 elements; recomputing rescans those shared elements needlessly. Only two elements ever change per slide.
Prove it to yourself
Time your solution on n = 10^6, k = 10^5. The recompute version times out; the incremental version finishes instantly. That gap IS the pattern.

What interviewers actually ask

Fixed-window questions are common warm-ups and set up the harder variable-window problems in L10–L11.

  • Maximum Average Subarray I (643) · Easy · Amazon — probes: do you slide with add/drop or naively recompute? Follow-up: what if k can change between queries (precompute prefix sums, L12)?
  • Best Time to Buy and Sell Stock (121) · Easy · Amazon, Microsoft — probes: a running-min one-pass, the degenerate window. Follow-up: multiple transactions (122), then with cooldown (309).
  • Find All Anagrams in a String (438) · Medium · Meta, Amazon — probes: fixed window + count-match in O(1) per slide. Follow-up: return count of matches vs the indices.
  • Permutation in String (567) · Medium · Meta, Amazon — probes: fixed window equal to a permutation via a match counter. Follow-up: case-insensitive / unicode.
  • Sliding Window Maximum (239) · Hard · Amazon, Google — probes: max is NOT O(1)-updatable — you need a monotonic deque (this is why L18 exists). Follow-up: sliding minimum, or the median (harder).
  • Maximum Number of Vowels in a Substring of Given Length (1456) · Medium · Amazon — probes: fixed window over a boolean predicate (is-vowel). Follow-up: generalise to any character class.

Universal probe: "can you avoid recomputing the window each time?" If you say yes and code the add/drop cleanly, you've passed the fixed-window bar.

Worked solutions

1. Maximum Average Subarray I (643)

Plain words: given an array and integer k, find the contiguous subarray of length k with the largest average, and return that average.

Brute force: for each of the n − k + 1 starts, sum k elements and divide. O(n·k). Insight: max average ↔ max sum (k is fixed), and the sum slides in O(1).

def findMaxAverage(nums, k):
    window = sum(nums[:k])           # first window, O(k)
    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 / k                  # convert the best sum to an average

Complexity: O(n) time, O(1) memory. Follow-up — "answer many queries with different k": precompute a prefix-sum array once, then each window sum is a subtraction (L12).

Try itReturn the STARTING INDEX of the max-average window, not the average.
def maxAvgStart(nums, k):
    window = sum(nums[:k])
    best, best_start = window, 0
    for right in range(k, len(nums)):
        window += nums[right] - nums[right - k]
        if window > best:
            best, best_start = window, right - k + 1
    return best_start
💡 Hint · Track best_start alongside best; update both when window beats best.

2. Find All Anagrams in a String (438)

Plain words: given strings s and p, return the start indices of every substring of s that is an anagram of p (same letters, any order).

Brute force: for each window, sort or count and compare to p's counts — O(n·k) or worse. Insight: keep a live Counter of the current window; compare to p's Counter. Comparing two Counters is O(alphabet) = O(1) for lowercase letters.

from collections import Counter
 
def findAnagrams(s, p):
    if len(p) > len(s):
        return []
    need = Counter(p)
    win = Counter(s[:len(p)])        # first window
    res = []
    if win == need:
        res.append(0)
    for right in range(len(p), len(s)):
        win[s[right]] += 1                 # entering char
        left = s[right - len(p)]           # leaving char
        win[left] -= 1
        if win[left] == 0:
            del win[left]                  # keep counts clean for == to work
        if win == need:
            res.append(right - len(p) + 1) # window start index
    return res

Complexity: O(n) time, O(1) extra (26-letter map). Follow-up — "just count matches": replace the list with an integer counter. A faster variant tracks a single matches integer instead of comparing whole Counters (the L11 technique).

3. Best Time to Buy and Sell Stock (121)

Plain words: one buy then one later sell; maximise profit. This is a degenerate window — a running minimum.

def maxProfit(prices):
    lowest = float('inf')         # cheapest price seen so far (left edge candidate)
    best = 0
    for price in prices:          # right edge sweeps forward
        lowest = min(lowest, price)   # update the best day to have bought
        best = max(best, price - lowest)  # best sale today
    return best

Complexity: O(n) time, O(1) memory. Follow-up — "as many transactions as you like" (122): sum every positive day-to-day gain.

Tradeoff

The tradeoff
Answer 'aggregate over every size-k window' efficiently.
Naive recompute per window
+ you gain Trivial to write; obviously correct.
− you pay O(n·k) — times out on large n or large k.
pick when Only for tiny inputs or a first sanity pass before optimising.
Incremental sliding window
+ you gain O(n) time, O(1) space for sum/count/boolean aggregates.
− you pay Needs a decomposable aggregate; min/max don't qualify directly.
pick when The default whenever the aggregate has a cheap inverse (sum, count, vowel-count).
Prefix sums
+ you gain O(1) per query for ARBITRARY window lengths after O(n) precompute.
− you pay O(n) extra memory; only for sums, not general aggregates.
pick when When k varies across many range-sum queries (L12).
What a senior engineer actually does
Incremental window is the default for a single fixed k. Switch to prefix sums when k varies, and to a monotonic deque when the aggregate is a max/min.

Where this shows up

Fixed-window sums are the basis of the simple moving average used everywhere in signal processing and finance dashboards: a moving average of period k is exactly this add-one-drop-one sum divided by k. Streaming metrics systems (rate-per-last-k-seconds) use the same incremental update so that each new data point costs O(1) rather than re-summing a buffer.

Checkpoint

You can now…
  • Explain why adjacent size-k windows differ by exactly two elements.
  • Write the add-entering / drop-leaving update from a blank file.
  • State that the entering index is `right` and the leaving index is `right − k`.
  • Maintain a live Counter over a fixed window and keep it clean.
  • Recognise that max/min windows need a deque, not a running variable.
  • Choose between incremental window and prefix sums based on whether k varies.

Quiz

Recall check · click to reveal
★ = stretch question

Practice queue

+7d / +21d mark spaced-repetition anchors.