VISHAL CHERUPALLYVC
Chapter 02  ·  Sliding Window

One In.
One Out.
Keep Going.

The sliding window is a scanning technique. A fixed-size frame moves across your data — one element enters on the right, one leaves on the left. You never recompute from scratch.

scroll to begin

Interactive Simulation

Watch the sliding window process an array to find the maximum sum of any contiguous subarray of size k = 3.

Live Visualizer — array sum, k = 3
R
3
0
1
1
4
2
1
3
5
4
9
5
2
6
6
7
Window
building
Current Sum
Max Sum
Speed
Inside window
Entering (right adds)
Leaving (left removes)
Current max window

A Pattern with Four Moving Parts

Every sliding window problem asks you to define four things. Once you know these four, you can solve any variant — the skeleton stays the same, only the definitions change.

  • 1
    What's in my window?
    This defines your state — a sum, a count, a frequency map, a set. Whatever you need to track about the elements currently inside the frame.
  • 2
    How does state change when the right pointer enters?
    This is your add() step. A new element just walked in. Update state.
  • 3
    How does state change when the left pointer leaves?
    This is your remove() step. Undo the effect the left element had on state. The array itself is untouched — only your bookkeeping changes.
  • 4
    When does the left pointer move?
    For fixed-size windows: every step, in lockstep with right, once the window is full. For variable windows: whenever a constraint is violated.

The Universal Skeleton

This is the template every problem in this series will follow. The four functions are the only things that change.

def solve(arr, k):
    left  = 0
    state = init()          # ← Q1: what does your window hold?

    for right in range(len(arr)):

        # ─── STEP 1: new element enters on the right ───
        add(arr[right])

        # ─── STEP 2: window is now full ────────────────
        if right >= k - 1:

            # ─── STEP 3: use the window ────────────────────
            use_window()

            # ─── STEP 4: oldest element leaves on the left ─
            remove(arr[left])
            left += 1

The one rule: Only read the window when it is fully formed — when right >= k - 1. Reading before that is the single most common bug in sliding window code.

How the two pointers actually move

The window is just two indices — left and right — scanning through the same array. They never go backwards.

indices:  0   1   2   3   4   5   6   7
array:  [ 3,  1,  4,  1,  5,  9,  2,  6 ]

step 1:   L                ← right=0, size 1, growing
          R

step 2:   L───R            ← right=1, size 2, growing

step 3:   L───────R        ← right=2, FULL. use(). then slide.

step 4:       L───────R    ← right=3, FULL. use(). slide.

step 5:           L───────R    ← right=4, FULL. use(). slide.

Once full: left and right move in lockstep. Size never changes. Only position does.

🔍 What "remove" actually means: There is no array deletion. remove(arr[left]) simply undoes the effect that element had on your state variable. The array stays intact. Only your bookkeeping moves forward.

Fixed vs. Variable Windows

All sliding window problems fall into one of two families. The problems in this series all use fixed-size windows.

TypeWhat stays constant?When does left move?Example
Fixed 🔒Window size = k (always)Every step, in lockstep with rightLC 643, 1343, 1456, 1100, 1652
Variable 🔓A constraint (validity condition)Only when the constraint is violatedLC 3, 209, 76 — covered next

LC 219 in this series is a small hybrid — it uses a set to check membership within distance k, so removal is index-triggered. You'll see exactly how that plays out.

Each one teaches the pattern a new trick

The skeleton never changes. Read each problem by asking: what is different from the one before it? That question is the whole lesson.

LC 643
Maximum Average Subarray I
The baseline. This is the skeleton, nothing else.

What it asks: Given an integer array and a window size k, find the contiguous subarray of length k with the maximum average. Return that average.

Statetotal = 0+ one integer tracking the best sum seen

The window holds a running sum. Every slide is a two-number transaction: subtract the element that just left, add the element that just arrived. Everything else in the window is unaffected. At the end, divide by k exactly once.

💡 Track max_sum, not max_avg. Division is expensive and floaty. A bigger sum means a bigger average, so compare sums. Divide once at the very end.

def findMaxAverage(nums, k):
    total   = 0
    max_sum = float('-inf')
    left    = 0

    for right in range(len(nums)):
        total += nums[right]            # ADD: new element enters

        if right >= k - 1:               # window is full
            max_sum = max(max_sum, total) # USE: record best
            total  -= nums[left]          # REMOVE: old element leaves
            left   += 1

    return max_sum / k                 # divide ONCE at the end

Common Pitfalls

  • Dividing inside the loop on every step — unnecessary and slow. Divide once at the end.
  • Storing max_avg (a float) instead of max_sum (an integer). Float comparison accumulates rounding error.
643 asks "what is the best window?" — a single maximum. 1343 asks a different question with the same window: instead of finding the best one, count how many pass a threshold.
LC 1343
Subarrays of Size K with Average ≥ Threshold
Same window. Different question. One line changes.

What it asks: Count the number of subarrays of length k whose average is greater than or equal to a given threshold.

← What changed from LC 643

sameadd(x) → total += x
sameremove(x) → total -= x
changeduse() → if total >= target: count += 1
Everything else is structurally identical. The skeleton is untouched.
Statetotal = 0+ a counter for windows that qualify

💡 Eliminate the division entirely. avg ≥ threshold is equivalent to sum ≥ k × threshold. Multiply once at the start, compare integers forever. No floats, no division inside the loop.

def numOfSubarrays(arr, k, threshold):
    target = k * threshold          # multiply once, avoid floats forever
    total  = 0
    count  = 0
    left   = 0

    for right in range(len(arr)):
        total += arr[right]

        if right >= k - 1:
            if total >= target:          # USE: does this window qualify?
                count += 1              # ← this is the only changed part
            total -= arr[left]
            left  += 1

    return count

Common Pitfalls

  • > vs >= — the problem says "greater than or equal to". Re-read the condition before submitting.
  • Sliding before scoring the window — this counts the next window, not the current one. Always: add → check window size → use → remove → advance left.
So far the window has held numbers. 1456 switches the element type to characters — but the window doesn't care. A vowel is just a 1. A consonant is a 0. The sum is the vowel count.
LC 1456
Maximum Vowels in a Substring of Length K
Characters instead of numbers. The window doesn't care.

What it asks: Given a string and window size k, find the maximum number of vowels in any substring of length k.

← What changed from LC 1343

changedadd: if s[right] in vowels: cnt += 1
changedremove: if s[left] in vowels: cnt -= 1
changeduse: max_cnt = max(max_cnt, cnt)
The skeleton order is identical. The state is still a single integer. Only what increments it changed.
Statecnt = 0vowel count inside the window (an integer, not a sum)

This is LC 643 in disguise. Replace "value of the element" with "is it a vowel (1) or not (0)". The running sum IS the vowel count. The add() and remove() steps just become conditional.

def maxVowels(s, k):
    vowels  = set('aeiou')
    cnt     = 0
    max_cnt = 0
    left    = 0

    for right in range(len(s)):
        if s[right] in vowels:          # ADD: only count vowels
            cnt += 1

        if right >= k - 1:
            max_cnt = max(max_cnt, cnt)
            if s[left] in vowels:         # REMOVE: only undo vowels
                cnt -= 1
            left += 1

    return max_cnt

Common Pitfalls

  • Unconditionally decrementing cnt for the leaving element — you only decrement if it was a vowel. The if guard is essential.
  • Using s[right] == 'a' or == 'e' ... — just use a set lookup. Cleaner and O(1).
So far, state was a single integer. 1100 upgrades the state to a frequency map — tracking how many of each character is currently in the window. This unlocks the ability to detect uniqueness.
LC 1100
Find K-Length Substrings With No Repeated Characters
State upgrades from one integer to a frequency map.

What it asks: Given a string and window size k, count the number of substrings of length k where all characters are unique.

← What changed from LC 1456

changedstate: freq = defaultdict(int) + distinct = 0
changedadd: freq[c]+=1; if freq[c]==1: distinct+=1
changedremove: freq[d]-=1; if freq[d]==0: distinct-=1
changeduse: if distinct == k: count += 1
The structure (add → if full → use → remove → advance) is unchanged.
Statefreq: dict+ distinct: int — characters with freq > 0

The key insight: if k characters fit into exactly k slots and distinct == k, there can be no repeats — each slot must hold a different character. You don't need to check every pair; just track how many distinct characters are in the window.

🔑 The distinct counter trick: Only update distinct when a frequency crosses the threshold — entering at 1 means a new character appeared; leaving at 0 means it's completely gone. Don't increment/decrement on every change, only on these crossings.

from collections import defaultdict

def countKLengthSubstrings(s, k):
    if k > 26:                         # only 26 lowercase letters — impossible
        return 0

    freq     = defaultdict(int)       # ← state upgraded to a frequency map
    distinct = 0                       # chars currently in window with freq > 0
    count    = 0
    left     = 0

    for right in range(len(s)):
        c = s[right]
        freq[c] += 1
        if freq[c] == 1:                 # first time c appears in window
            distinct += 1

        if right >= k - 1:
            if distinct == k:           # all characters are unique
                count += 1

            d = s[left]
            freq[d] -= 1
            if freq[d] == 0:            # c is fully gone from window
                distinct -= 1
            left += 1

    return count

Common Pitfalls

  • Forgetting to decrement distinct when freq[d] hits exactly 0. If you decrement unconditionally, distinct goes negative for duplicates.
  • Confusing this with the variable-window problem "longest substring with at most k distinct" — that shrinks the window. This one holds it fixed.
1100 needed a full frequency map to answer "how many of each?". 219 asks only "is this value anywhere in the last k positions?" — a presence check, not a count. A set is enough. State shrinks back down.
LC 219
Contains Duplicate II
Use a set. Check before you add. The window is defined differently.

What it asks: Return True if any two equal values in the array exist at indices no more than k apart.

← What changed from LC 1100

changedstate: seen = set() (set, not a map)
changeduse: check BEFORE adding (if val in seen: return True)
changedremoval trigger: when right >= k (not k-1)
The window concept is the same, but the removal rule and use-order are different — because the window is defined by distance, not length.
Stateseen: setpresence check only — no counts needed

This problem doesn't say "substrings of size exactly k". It says "any two equal values within distance k". The window holds the last k values seen. If the current value is already in there, you found a pair. Check membership before inserting.

Order matters here. The use() step (the duplicate check) happens before add(). You're checking whether the incoming value collides with what's already in the window, before you actually add it.

def containsNearbyDuplicate(nums, k):
    seen = set()                        # ← state is a set, not a map

    for right in range(len(nums)):

        if nums[right] in seen:          # USE first — before adding
            return True
        seen.add(nums[right])            # ADD

        if right >= k:                   # REMOVE when window exceeds k
            seen.remove(nums[right - k])

    return False

Why a set and not a map?

StructureAnswers the questionUse when
freq map"How many of each are in the window?"LC 1100 — you need counts
set"Is this value anywhere in the window?"LC 219 — you only need presence

Common Pitfalls

  • Off-by-one on the removal trigger: the window holds at most k previous elements, so remove at right >= k (not k - 1).
  • Removing the wrong index — nums[right - k] is the element that entered exactly k steps ago. Off by one here causes a silent wrong answer.
All five problems so far work on linear arrays. 1652 bends the array into a circle — the window can wrap around the end back to the beginning. One operator handles it: % n.
LC 1652
Defuse the Bomb
The array is circular. Modulo makes the window wrap.

What it asks: Given a circular code array, replace each element code[i] with the sum of the next k elements (if k > 0) or the previous |k| elements (if k < 0). If k = 0, all values are zero.

← What changed from LC 643

changedall indices now use (i % n) for circular access
changedwindow direction depends on sign of k
changedfirst window is built once upfront, then slid n times
The core sum-window pattern is identical to LC 643. Only the indexing arithmetic and window direction changed.
Statetotal = sumexactly as in 643 — but indices wrap via % n

The "circular" part only lives in the indexing. Build the first window once — compute its sum directly. Then for each of the n positions: record the sum, subtract the element that just left, add the element that just arrived — both using % n to wrap around.

🔑 Build first, then slide. Computing the first window takes O(|k|). After that, every slide is O(1) — one subtraction, one addition. Don't recompute from scratch each time.

def decrypt(code, k):
    n      = len(code)
    result = [0] * n

    if k == 0:
        return result                    # all zeros

    # Set window direction based on sign of k
    if k > 0:
        start, size = 1, k               # next k elements
    else:
        start, size = n - abs(k), abs(k) # previous |k| elements

    # Build the first window once
    total = sum(code[(start + j) % n] for j in range(size))

    for i in range(n):
        result[i] = total               # USE: record answer for this position
        total -= code[(start + i) % n]         # REMOVE: left element exits
        total += code[(start + i + size) % n]   # ADD: right element enters

    return result

Common Pitfalls

  • Mixing up which index is subtracted vs added during the slide — draw it out once on paper before coding.
  • In languages other than Python, negative modulo can return a negative number. Use (idx % n + n) % n to be safe in C++/Java.
  • Recomputing the window from scratch for each position — that's O(n·|k|). Build once, slide in O(1).

All Problems at a Glance

Every problem differs in exactly two places: what the window holds, and what the use() step does.

ProblemWindow holdsState typeuse() triggers whenOutput
LC 643Sum of elementsintright ≥ k−1 → track max summax / k
LC 1343Sum of elementsintright ≥ k−1 and sum ≥ k×tcount
LC 1456Vowel countintright ≥ k−1 → track max countmax vowels
LC 1100Frequency map + distinct counterdict + intright ≥ k−1 and distinct == kcount
LC 219Set of last k valuessetval in seen (before add)bool
LC 1652Circular sum (mod n)intevery position iarray

Debug Checklist

When your solution is "almost right" but fails test cases, go through this list in order.

  • Step order: Add → (check window full) → Use → Remove → Advance left. Not Use before the window is full.
  • Off-by-one: Window is full when right >= k - 1, not right >= k. Test with k=1 mentally.
  • State invariant: At any point during the loop, can you describe in one sentence what your state variable represents? If you can't, it's drifting.
  • Remove correctly: You are undoing the effect — not deleting the element. Subtract what was added, seen.remove() what was add()'d, decrement what was incremented.
  • Distinct counter: Only update when crossing 0 or 1. Not on every frequency change.
  • Circular arrays: Every index that could exceed bounds must be wrapped with % n.
One pointer enters.
One pointer leaves.

The window slides forward. You never look back. Variable-size windows come next — where the window shrinks as well as grows.

← Back to Python track