Search Tech Journey

Find topics, journeys and posts

back to blog
algorithmsintermediate 60m read

L10 · Sliding Window II — Variable Size

Expand right unconditionally, shrink left while the invariant is broken, record at the right moment — and the amortisation argument that keeps it linear.

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

🎯 Own the variable-size window: grow right always, shrink left while invalid, and prove the whole thing is O(n) even though there are two nested-looking loops.

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

Watch first

What this pattern IS

In L09 the window was rigid — always exactly k wide. But most real questions ask for the longest or shortest window satisfying some condition: the longest substring with no repeated character, the shortest subarray whose sum is at least a target, the longest subarray with at most K distinct values.

Now the window must breathe. You don't know k in advance — you discover it.

The naive approach: try every start, extend as far as valid, record. That's O(n^2) (or worse with per-window checks). The sliding-window insight: a single left pointer and a single right pointer, each only ever moving forward. You grow the right edge to explore, and when the window becomes invalid you nibble the left edge until it's valid again. Because each pointer traverses the array at most once, the total work is O(n) — even though the code looks like a loop inside a loop.

A stretchy tape measure over a fence
🌍 Real world
You drag the right end of a tape along a fence, extending it. When the tape covers something forbidden, you reel in the left end just enough to exclude it, then keep extending right. Each end only moves one direction — you never rewind.
💻 Code world
for right in ...: add(nums[right]); while invalid: remove(nums[left]); left += 1; record().

First principles

From first principles
Start with the question
Why is the variable window O(n) when it has a while loop nested inside a for loop?
  1. 1
    The right pointer advances exactly once per outer iteration: n moves total.
    forced by · The `for right in range(n)` loop runs n times and right never moves backward.
  2. 2
    The left pointer only ever increases, and it can never exceed right (or n).
    forced by · The inner while only does `left += 1`; it never decrements, and it stops once the window is valid.
  3. 3
    So across the ENTIRE run, the inner while executes at most n times total, not n times per outer step.
    forced by · left has only n forward moves available for the whole algorithm — this is amortisation.
  4. 4
    Each add/remove of an element is O(1) with a hash map or counter.
    forced by · Insert, delete, and lookup in a dict are expected O(1).
  5. 5
    Total work = n right-moves + at most n left-moves + O(1) each = O(n).
    forced by · Two independent linear budgets, spent once, not multiplied together.
⇒ Therefore
Each pointer has an n-move budget spent once. Nested syntax ≠ nested cost — this is amortised O(n).

The templates

Template A — longest window satisfying a condition

def longest_valid(nums):
    left = 0
    best = 0
    state = init_state()               # e.g. a set or Counter of the window
    for right in range(len(nums)):
        add(state, nums[right])        # grow the window to include right
        while not valid(state):        # window broke the invariant -> shrink
            remove(state, nums[left])
            left += 1
        # here the window [left, right] is valid; record its length
        best = max(best, right - left + 1)
    return best

For longest, you record after restoring validity, so the recorded window is always valid.

Template B — shortest window satisfying a condition

def shortest_valid(nums, target):
    left = 0
    best = float('inf')
    total = 0
    for right in range(len(nums)):
        total += nums[right]           # grow
        while total >= target:         # window is valid -> try to shrink for a shorter one
            best = min(best, right - left + 1)   # record BEFORE shrinking
            total -= nums[left]
            left += 1
    return best if best != float('inf') else 0

For shortest, you record while the window is valid and shrink to find something even shorter — the record lives inside the while.

The mirror is the key mental split: longest records outside the while; shortest records inside it.

Mental model

Mental modelGrow right, shrink left
A caterpillar: the head (right) always crawls forward; when the body breaks a rule, the tail (left) catches up just enough. Both ends only move forward. Length = right − left + 1.
  • right advances every outer iteration, unconditionally.
  • while the window is INVALID (longest) or VALID (shortest), move left.
  • longest: record after the while (window valid). shortest: record inside the while.
  • Both pointers monotonically increase → amortised O(n).
🔔 Fires when you see
'longest/shortest subarray or substring such that ...', 'at most / exactly K distinct', 'no repeats', or 'sum ≥ / ≤ target'.

Memory / mnemonic

Carry the caterpillar and one placement rule: "LONGest records LATE, SHORTest records SOON."

  • Longest → the check that shrinks fires when the window is bad; by the time you record, it's good again → record after the while (late).
  • Shortest → the while fires when the window is good, and you're squeezing it smaller → record inside the while (soon), then shrink.

The pointer rhyme: "right runs the loop, left chases the rule." Right is driven by the for; left is driven by the invariant.

If you ever find yourself moving right backward, or left past right, you've abandoned the pattern — both indices only ever go up.

The trap

Common misconception
✗ What most people think
A while loop inside a for loop is O(n^2), so the variable sliding window is quadratic.
✓ What is actually true
It's O(n). The inner while's TOTAL iterations across the whole run are bounded by n, because left only moves forward and has just n moves to spend.
Why the myth is so sticky
Amortisation: you count left's moves over the entire algorithm, not per outer step. left advances at most n times total, independent of how the for loop iterates.
Prove it to yourself
Instrument a counter that increments on every `left += 1` and every outer step. On any input, both counters stay ≤ n. Their sum is ≤ 2n = O(n).

What interviewers actually ask

Variable windows are among the most-asked medium problems because they test the amortisation reasoning, not just code.

  • Longest Substring Without Repeating Characters (3) · Medium · Amazon, Adobe, Bloomberg — probes: variable window + a last-seen map; can you jump left correctly? Follow-up: at most K distinct characters.
  • Minimum Size Subarray Sum (209) · Medium · Amazon, Google — probes: the shortest-window template with record-before-shrink. Follow-up: what if numbers can be negative (window breaks — use prefix + something else, L12)?
  • Longest Repeating Character Replacement (424) · Medium · Google, Amazon — probes: window validity via (window length − maxfreq ≤ k). Follow-up: why you never need to decrement maxfreq.
  • Fruit Into Baskets (904) · Medium · Amazon — probes: 'at most 2 distinct' framed as a story; the K-distinct template. Follow-up: generalise to K baskets.
  • Minimum Window Substring (76) · Hard · Meta, Amazon, Uber — probes: shrink with a need-map and a satisfied counter; the boss of the pattern. Follow-up: multiple valid windows, or streaming input.
  • Longest Substring with At Most K Distinct Characters (340) · Medium · Amazon, Meta — probes: the K-distinct longest template directly.

Universal escalation: "can you prove it's linear?" Have the amortisation argument ready in one sentence.

Worked solutions

1. Longest Substring Without Repeating Characters (3)

Plain words: find the length of the longest substring of s that contains no repeated character.

Brute force: check every substring for uniqueness — O(n^2) substrings times O(n) check = O(n^3), or O(n^2) with a set per start. Insight: keep a set of the current window's characters; when the entering char is already present, shrink from the left until it's gone.

def lengthOfLongestSubstring(s):
    seen = set()
    left = 0
    best = 0
    for right in range(len(s)):
        while s[right] in seen:        # entering char duplicates -> shrink
            seen.remove(s[left])
            left += 1
        seen.add(s[right])             # now safe to add
        best = max(best, right - left + 1)   # record (longest -> after the shrink)
    return best

Complexity: O(n) time, O(min(n, alphabet)) memory. Follow-up — "return the substring itself": track best_left when you update best, then slice.

Try itUse a last-seen-index map to jump left in one move instead of nibbling one char at a time.
def lengthOfLongestSubstring(s):
    last = {}
    left = best = 0
    for right, ch in enumerate(s):
        if ch in last and last[ch] >= left:
            left = last[ch] + 1        # jump past the previous occurrence
        last[ch] = right
        best = max(best, right - left + 1)
    return best
💡 Hint · Store last[char] = index; on a repeat, set left = max(left, last[char] + 1).

2. Minimum Size Subarray Sum (209)

Plain words: given positive integers and a target, return the length of the shortest contiguous subarray whose sum is ≥ target, or 0 if none.

Insight: grow right to reach the target; once reached, shrink left greedily to find the shortest still-valid window — record inside the while.

def minSubArrayLen(target, nums):
    left = 0
    total = 0
    best = float('inf')
    for right in range(len(nums)):
        total += nums[right]           # grow
        while total >= target:         # valid -> try shorter
            best = min(best, right - left + 1)   # record before shrinking
            total -= nums[left]
            left += 1
    return best if best != float('inf') else 0

Complexity: O(n) time, O(1) memory. Follow-up — "what if values can be negative?": the window no longer has the monotonic property (adding elements can decrease the sum), so this breaks — switch to prefix sums + a monotonic structure or a different method (L12).

3. Longest Repeating Character Replacement (424)

Plain words: given a string of uppercase letters and an integer k, you may replace at most k characters; return the longest substring that can be made all one letter.

Insight: a window is valid if (window_length − count_of_most_frequent_char) ≤ k — that difference is how many replacements you'd need. Keep a Counter and the running max frequency; shrink when the window becomes invalid.

from collections import Counter
 
def characterReplacement(s, k):
    count = Counter()
    left = 0
    maxfreq = 0
    best = 0
    for right in range(len(s)):
        count[s[right]] += 1
        maxfreq = max(maxfreq, count[s[right]])   # most common char's count in window
        while (right - left + 1) - maxfreq > k:   # too many replacements needed -> shrink
            count[s[left]] -= 1
            left += 1
        best = max(best, right - left + 1)
    return best

Complexity: O(n) time, O(26) memory. Follow-up — "why never decrement maxfreq?": a stale (too-high) maxfreq can only make the window seem valid for longer, never shorter than the true best; since we only ever want a longer answer, an overestimate never lets us record something wrong.

Tradeoff

The tradeoff
Find a longest/shortest contiguous window under a condition.
Brute force all substrings
+ you gain Obviously correct; trivial to reason about.
− you pay O(n^2) or O(n^3) — times out past a few thousand elements.
pick when Only as a reference oracle to test the fast version.
Variable sliding window
+ you gain O(n) time; the expected answer for most 'longest/shortest subarray' problems.
− you pay Requires a MONOTONIC validity condition — adding elements must move validity one way only.
pick when Whenever growth/shrink has a monotone effect (all-positive sums, distinct-count, replacement count).
Prefix sums + hashmap
+ you gain Handles negatives and 'exactly K' counting where windows fail.
− you pay O(n) memory; conceptually heavier.
pick when When the window property is NOT monotonic (negatives, exact-sum counts) — see L12.
What a senior engineer actually does
Reach for the variable window when growing/shrinking changes validity monotonically. The moment negatives or 'exactly K subarrays counting' appears, that assumption breaks — go to prefix sums.

Where this shows up

Variable windows model rate-limiting and connection throttling: a server keeps a window of recent request timestamps and shrinks it from the left as old requests age out, extending right as new ones arrive — exactly the grow-right/shrink-left shape. TCP's congestion control also maintains a window that grows and contracts based on a validity signal (packet loss), the same expand-until-invalid idea applied to network flow.

Checkpoint

You can now…
  • Write the longest-window and shortest-window templates from a blank file.
  • Place the record statement correctly: after the while for longest, inside it for shortest.
  • Explain the amortised-O(n) argument in one sentence.
  • Recognise when a validity condition is monotonic enough for a window to work.
  • Solve longest-no-repeat with both the nibble and the jump variants.
  • Know that negatives or exact-count problems break the window and need prefix sums.

Quiz

Recall check · click to reveal
★ = stretch question

Practice queue

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