sliding window · study console
Arrays · Strings · Sliding Window

Grow right.
Shrink left.
Never miss.

The complete technique — from brute-force motivation to shrink-condition traps to monotonic deque max — calibrated for Tier-1 custom problem banks.

fixed window variable expand/shrink frequency map monotonic deque stacked constraints
§ 01 — draw the line first

Sliding window vs two pointers

You've already covered two pointers. The distinction matters more than it looks, so here it is as a crisp two-sentence recap rather than a re-teach.

the distinction

Two pointers is a general technique: two indices moving toward or away from each other for any structural reason — converging on a pair, partitioning in place, compacting. Sliding window is specifically about maintaining a contiguous subarray or substring whose contents satisfy some property, where you expand the right edge to grow the window and shrink the left edge only to restore validity — and the window's contents are the answer (or its best-seen state).

The fast/slow two-pointer pattern is the mechanical cousin — both pointers move forward only — but fast/slow tracks a write boundary against a uniqueness predicate, not a bounded, property-constrained window. The moment the answer is "a contiguous run satisfying a condition," reach for sliding window.

§ 02 — earn the technique

Derived, not memorized

The canonical entry point is longest substring without repeating characters. Don't jump to the solution — derive it from the brute force and find the redundant work.

The O(n³) brute force

For every pair of indices (i, j), slice out the substring and scan it for duplicates. That's O(n²) pairs, each requiring O(n) duplicate detection — O(n³) total. The obvious O(n²) upgrade: keep a running set as you extend j outward from each fixed i. Cheaper check, same outer loop.

O(n²) with a set — still too slow
for i from 0 to n - 1:          # fix left edge
    seen = empty set
    for j from i to n - 1:      # extend right edge
        if s[j] in seen: break  # duplicate found, give up
        seen.add(s[j])
        best = max(best, j - i + 1)
# when i advances by 1, we throw away the entire seen set and rebuild from scratch
# that's the waste: everything we learned about the substring starting at i+1

What work is repeated?

When i increments from 0 to 1, the inner loop restarts and re-scans characters starting at index 1 that were already examined. The "window of valid characters" already exists in memory — we just threw it away and rebuilt it. The insight: instead of resetting left and rebuilding, shrink the left edge by one and update the set incrementally.

the amortized O(n) argument

The right pointer moves forward exactly n times total across the entire algorithm — it never goes back. The left pointer moves forward at most n times total, across the entire algorithm, not per step of right. Two pointers, each moving at most n steps forward — O(n) total work, despite the appearance of a nested loop structure. This is the amortized argument: you're prepaying the left-pointer movement across all the right-pointer steps.

§ 03 — state it, don't gesture at it

The core invariant

Every correctly implemented sliding window maintains one invariant. State it explicitly before writing a line of code — the invariant tells you where to record the answer and which direction to shrink:

the invariant

The window [left, right] always represents a currently valid (or currently best-candidate) range. We grow right to explore more elements. We shrink left only to restore validity — never speculatively, never to "try" something.

The corollary: where you record the answer determines which variant you're implementing. For "longest window satisfying property P" — record after the shrink loop, when validity is restored. For "smallest window satisfying P" — record while the window is still valid, inside the shrink loop, before shrinking further. Getting these two placements swapped is the second most common interview bug after the if/while error below.

maximize window sizeExpand right. Check validity. If invalid, shrink left until valid again. Then record right - left + 1 as a candidate best. The answer-recording step comes after the shrink loop.
minimize window sizeExpand right until valid. Then, while still valid, record the window size and shrink left to try smaller. The answer-recording step comes inside the shrink loop.
count windows satisfying propertyOften requires the "at most K minus at most K-1" trick (see Problem 9), not a direct count. Direct counting of exact-K windows is the hardest variant and requires a reframing, not a simple placement change.
§ 04 — where bugs live under pressure

The shrink-condition trap

This gets disproportionate attention because it's the most reliable source of wrong answers in sliding window problems at Tier-1 interviews — even among candidates who understand the pattern conceptually.

if vs while: when the assumption breaks

Using if to shrink works correctly in exactly one narrow case: when adding a single new element to the right can cause at most one violation, and removing a single element from the left always restores validity in one step. For simple sum-bounded windows with a fixed maximum, this holds. For anything involving frequency maps, distinct character counts, or multiple constraints, it doesn't.

the wrong assumption behind using `if`

"Shrinking left by one step always restores validity." This is only true when the validity condition is monotonic in the window size and the constraint involves a single cumulative aggregate. For frequency-map problems — where removing an element at left might still leave duplicates deeper in the window — a single left-shrink step may not restore validity. Only while guarantees you keep shrinking until the window is valid.

Side-by-side trace: string "abcba", target: longest without repeats

Watch what happens when a second occurrence of 'a' enters the window at right=4. The window is "abcba". Left is at 0.

❌ using if — broken
✓ using while — correct
right=4, s[4]='a', freq['a']=2 → violation
right=4, s[4]='a', freq['a']=2 → violation
if freq['a'] > 1: remove s[left]='a', left=1
→ freq['a']=1. Shrink stops.
Window: "bcba" — still has duplicate 'b'!
Bug: left moved only once, second 'b' not found.
while freq['a'] > 1:
  remove s[left], left++
Step 1: remove 'a'(left=0), left=1, freq['a']=1 ✓
while exits. Window: "bcba"… wait, is 'b' duplicated?
Re-check: freq['b']=2? → while loops again
remove 'b'(left=1), left=2, freq['b']=1 ✓
Window: "cba" — valid ✓

Note: The trace above is for a string where two characters become duplicated simultaneously. A correct while-loop continues shrinking until all violations are resolved — not just the most recent one. The off-by-one in window length is separate: always use right - left + 1 for inclusive endpoints, not right - left.

the off-by-one: right − left + 1

Window from index left=2 to right=5 contains indices 2, 3, 4, 5 — four elements. right - left = 3 — wrong. right - left + 1 = 4 — correct. Always +1 for inclusive endpoints. Forgetting this burns a test case at the worst moment.

§ 05 — make it visible

Widget A — expand/shrink animator

Step through the window expanding and shrinking on a real string. The frequency map sidebar updates live. Watch the red flash — that's the shrink trigger — and confirm the window is valid before the next expand step begins.

Widget A · expand / shrink · frequency map

variable window
array / string
window [L, R]
window size
0
best so far
0
press Step to begin
window left ptr right ptr shrink trigger
frequency map
§ 06 — see the structural difference

Widget B — fixed vs variable window

Two panels, same underlying array, two problems running in lockstep. Left: a fixed-size-k window sliding mechanically — no shrink, no condition, just slide. Right: a variable window expanding and shrinking for a different problem. The structural difference is what to internalize, not the specific problems.

Widget B · fixed vs variable · side by side

structural comparison
Fixed window (k=3)
max sum of k consecutive
current sum
max sum
Variable window
longest subarray with sum ≤ target
window sum
best length
target for variable: 15
fixed window variable window shrinking (variable only)
§ 07 — the hardest companion structure

Widget C — monotonic deque for window maximum

The sliding window maximum problem asks: for each window of size k, what is the maximum element? Naively, scanning the window for a max each time is O(nk). A monotonic deque (double-ended queue) maintains potential maximums in decreasing order, giving O(1) max lookup and O(n) total.

The deque stores indices, not values. Three invariants hold at all times: (1) elements are in decreasing value order front-to-back; (2) the front is always the current window maximum; (3) indices out of the window are evicted from the front.

Two eviction rules, both from different ends:

Pop from back (value eviction)Before pushing index right, pop all indices from the back whose values are ≤ arr[right]. They can never be the window max while arr[right] is in range — they are dominated and useless. This maintains the decreasing order invariant.
Pop from front (window eviction)After adding right, check if the front index is now out of the current window (front_index < right - k + 1). If so, evict it. The window slid past it.
Read the maxOnce the window is full (right ≥ k - 1), arr[deque.front] is the window maximum. The front is always the largest, valid, in-window element.

Widget C · monotonic deque · sliding window maximum

k = 3
deque (indices, front → back, decreasing values)
empty
current max
output so far
press Step to begin
current window (k=3) right pointer (entering) deque front (current max) evicted from deque
§ 08 — before you write the loop

Pattern recognition: the pre-code checklist

At Tier-1 interviews, the problem won't announce "use sliding window." Here's what to listen for, especially in stacked-constraint framing where the technique isn't obvious:

reach for sliding window when you see
  • "Longest / shortest subarray or substring" satisfying some property — the canonical framing. Any optimization over contiguous ranges is the signal.
  • "At most K distinct / repeated / of type X" — frequency-map window. The shrink condition is when the distinct count exceeds K.
  • "Subarray of exactly size k" / explicit window size given — fixed window, no shrink needed, just slide and maintain aggregate.
  • Stacked constraints: "longest substring where no character appears more than K times AND window cost stays under C" — these require a multi-condition shrink loop. The while condition is the OR/AND of all constraint violations.
  • "Minimum size subarray summing to ≥ target" — variable window shrinking in the opposite direction: shrink while still valid to minimize size. The shrink-direction flip is the trap.
  • "For each window of size k, what is the max/min?" — monotonic deque. Any per-window extreme query.
  • "Number of subarrays with exactly K distinct" — exact-K reframing: atMost(K) − atMost(K-1). A counting problem in disguise.
  • Streaming / contiguous-range framing — "as data arrives one record at a time, find the longest/shortest run satisfying..." — sliding window on streams.
§ 09 — beyond the interview

Sliding window in real systems

The sliding window pattern appears across systems engineering in three specific forms that are worth naming, because encountering them in production code without recognizing the interview pattern is a missed connection.

TCP congestion window. TCP's congestion control maintains a "window" of bytes that can be in-flight at once — sent but not yet acknowledged. The window grows when acknowledgments arrive (expand right) and shrinks on detected loss (shrink left). The invariant is identical to the interview problem: the window bounds a contiguous, valid, dynamically-resized range, and boundary movement is event-driven rather than clock-driven.

Streaming record validation. A streaming record validator that must never hold an entire dataset in memory is a direct real-world cousin of the fixed-size sliding window: rolling validity checks over "the last N records" — rate-of-violation-in-the-last-1000-records monitoring, for instance — use exactly the expand-and-evict discipline of a fixed-size window, where evicting the oldest record as a new one arrives is structurally the same as advancing the left pointer. The same amortized-O(n) argument applies: each record enters and leaves the window exactly once.

Statistical drift detection. Engineers building drift-detection tooling that bins a data stream into quantile buckets in a single pass are applying the same "maintain a running, bounded-memory summary as data flows past once" discipline that makes sliding window O(n) instead of O(n²). The amortized-pointer-movement argument from the interview technique is the same argument that justifies why a streaming statistics pass doesn't need to revisit old data — each data point contributes to the running summary once, then leaves the active window.

tumbling vs sliding in stream processing

In stream-processing frameworks (Flink, Spark Streaming, Kafka Streams), the interview's "sliding window" corresponds to a sliding window aggregation — a window that advances by a hop size smaller than its length, so windows overlap. The interview's "fixed window slid by one" is exactly this with hop=1. A tumbling window is the non-overlapping version — no element appears in two windows. Knowing both terms is useful vocabulary in any systems interview involving streaming analytics.

§ 10 — calibrated easy → hard

Problem set — 10

Ordered easy to hard. Fixed window first, then variable, then stacked-constraint and reframing problems. Four are explicitly marked for Widget A or C re-use — same visual vocabulary, different parameters, faster pattern recognition.

§ 11 — language-agnostic

Pseudocode templates

Three templates. Every annotation is a trap you will encounter, not a formality.

template 1 — fixed-size window
function fixed_window(arr, k):
    n = length(arr)
    if n < k: return NONE          # guard: array smaller than window

    window_val = aggregate(arr[0 .. k-1])  # seed the first window
    best = window_val

    for right from k to n - 1:
        # slide: add incoming element, remove outgoing element
        window_val = update(window_val, add=arr[right], remove=arr[right - k])
        best = max(best, window_val)

    return best
    # note: no shrink loop, no condition check — just slide by one each step
    # window size is always exactly k; right - left + 1 = k at all times
template 2 — variable-size window (maximize)
function variable_window_max(arr, condition):
    left = 0
    best = 0
    # initialize any window state: freq map, running sum, distinct count, etc.
    state = empty

    for right from 0 to n - 1:

        # ① EXPAND: unconditionally add arr[right] to state
        state.add(arr[right])

        # ② SHRINK: restore validity — use WHILE, never IF
        while NOT valid(state):          # ← WHILE, never if
            state.remove(arr[left])
            left = left + 1

        # ③ RECORD: window is now valid — record the best
        # placement here (AFTER while) is for maximize problems
        best = max(best, right - left + 1)    # ← +1 for inclusive

    return best

# for MINIMIZE problems: record INSIDE the while loop before shrinking
function variable_window_min(arr, condition):
    left = 0
    best = INFINITY
    state = empty

    for right from 0 to n - 1:
        state.add(arr[right])

        while valid(state):                 # shrink WHILE still valid (opposite!)
            best = min(best, right - left + 1)  # ← record BEFORE shrinking
            state.remove(arr[left])
            left = left + 1

    return best if best != INFINITY else -1
template 3 — monotonic deque for window max
function window_max(arr, k):
    n = length(arr)
    dq = empty deque                    # stores indices, values are decreasing
    output = []

    for right from 0 to n - 1:

        # STEP 1: evict from back — remove dominated elements
        while dq not empty AND arr[dq.back()] <= arr[right]:
            dq.pop_back()               # smaller/equal elements can never win
        dq.push_back(right)

        # STEP 2: evict from front — remove out-of-window indices
        if dq.front() < right - k + 1:
            dq.pop_front()              # index slid out of the window

        # STEP 3: output max once window is full
        if right >= k - 1:
            output.append(arr[dq.front()])  # front is always current max

    return output
    # note: "less than" vs "less than or equal" in step 1 matters when values tie
    # using <= means equal elements at the back are evicted — the newer one is kept
    # this is correct: an older equal element would leave the window sooner
§ 12 — curated, not a dump

Further reading

Amortized analysis — why O(n) despite a nested loopCLRS (Introduction to Algorithms), §17.1–17.2 introduces the aggregate and accounting methods for amortized analysis — the formal framework for the argument that "right moves n times, left moves n times, total is O(n)." For a lighter but rigorous treatment, Skiena's Algorithm Design Manual covers amortized analysis in the context of dynamic arrays. The key insight to internalize: amortized O(1) per operation is not a per-step guarantee, it's a global budget argument — exactly why the nested while loop in sliding window is still O(n) total.
Monotonic structures — the general familyThe monotonic deque in Widget C is one instance of a broader family: monotonic stack (next greater element, largest rectangle in histogram), monotonic deque (sliding window max/min). A dedicated monotonic stack session covers this fully. For now, the critical thing is recognizing the pattern: maintaining a structure where elements are in a monotonic order, evicting those that can never again be useful. Leetcode's problems 739 (Daily Temperatures), 84 (Largest Rectangle in Histogram), and 1438 (Longest Continuous Subarray with Absolute Diff ≤ Limit) are the natural follow-ons from this session.
TCP congestion window — the real-world shapeRFC 5681 (TCP Congestion Control) describes the cwnd (congestion window) mechanism in full. The slow-start and congestion-avoidance phases are literally "expand the window until a violation, then shrink" — the same invariant as variable sliding window. For stream-processing vocabulary, the Apache Flink documentation on windowing (tumbling, sliding, session windows) connects these ideas to production data engineering. The vocabulary overlap is not coincidental — stream processing borrowed the term from TCP.
How to use this: before writing the loop for any candidate problem, say the invariant out loud and identify the shrink condition explicitly. If the shrink condition involves multiple properties, write the full while condition before touching the loop body. The two most reliable ways to fail a sliding window problem under time pressure are (1) using if when you need while, and (2) recording the answer at the wrong moment relative to the shrink loop. Both are eliminated by stating the invariant and window goal — maximize or minimize — before typing.