Chapter 5
22 min read
Section 28 of 261

Sliding Window Technique

Arrays

Many array and string problems ask the same question in different costumes: among all contiguous sub-segments of length / weight / property X, which is best? The brute-force answer is to enumerate every interval [l,r][l, r] and inspect it — an instant O(n2)O(n^2) orO(n2k)O(n^2 \cdot k) blow-up that fails the momentnn hits a few thousand. The sliding window technique replaces that quadratic with a single linear pass by exploiting one observation: as the right endpoint moves forward, the left endpoint never has to move backward. Two monotone pointers, total workO(n)O(n), and a state object that updates incrementally as the window slides.

Sliding window is the workhorse behind moving-average filters in digital signal processing, rate limiters in distributed systems, rolling RMS in audio, k-mer scanning in genomics, time-windowed analytics on infinite streams, and a sizeable fraction of the array and string questions you will be asked in interviews. By the end of this section you will recognize when the technique applies, when it does not, and you will have written it from the universal template six times in two languages.


The Core Idea: A Window That Walks

Define a window as a contiguous rangeA[l..r]A[l \,..\, r] withlrl \le r (or the empty window whenr<lr < l). A summary SS of the window — its sum, its character histogram, its set of distinct elements, its maximum — is what we care about. The technique rests on three operations:

  1. Add: extend by one on the right,SSA[r+1]S \leftarrow S \oplus A[r{+}1]. Cost is typically O(1)O(1).
  2. Remove: shrink by one on the left,SSA[l]S \leftarrow S \ominus A[l]. Cost is typically O(1)O(1).
  3. Query: read the current answer fromSS in O(1)O(1).

If your problem can be modeled this way, sliding window applies. Concrete summaries:

ProblemSummary SAdd A[r]Remove A[l]Query
Max sum, fixed krunning sum+= A[r]−= A[l]S
Longest no-repeathashmap last_seenlast_seen[c] = r(implicit via l-jump)r − l + 1
Min window substringCounter need + missingneed[c]−−; if was needed missing−−need[c]++; if now needed missing++missing == 0?
Sum at most K (positive)running sum+= A[r]−= A[l]S ≤ K?
Sliding maxmonotonic dequeevict tails ≤ A[r]; push rexpire head if past lA[dq[0]]
Permutation in stringfreq diff vector + match countdiff at A[r]−−diff at A[l]++matches == |alphabet|?
Mental model. Think of the window as a centipede crawling along the array. Its head rr probes forward; its tail ll follows. Neither head nor tail ever reverses. The summary SS is what the centipede remembers about the food it has eaten so far.

Why It Is O(n): The Amortized Argument

At first glance the variable-size window looks suspect: an iteration of the outer loop can run an arbitrarily long inner loop that pushesll forward many steps. Could the worst case be O(n2)O(n^2)?

No — and the proof is the textbook example of aggregate amortized analysis. Charge the algorithm for each pointer move:

  1. The right pointer rr advances exactly once per outer loop iteration. Total moves ofrr: at most nn.
  2. The left pointer ll only ever increases (it never decreases). It is bounded above bynn. Total moves ofll across the whole algorithm: at mostnn.
  3. Per-move cost is O(1)O(1) for sums, hashmap updates, and deque operations.

Total: O(n)+O(n)=O(n)O(n) + O(n) = O(n). The fact that sometimes the inner loop fires hard and sometimes it does not is irrelevant to the aggregate bound — we paid for each pointer move at most once.

Why this is amortized, not worst-case-per-step

A particular outer iteration can doΘ(n)\Theta(n) work (when the inner shrink loop fires for many steps at once). The per-iteration worst case isO(n)O(n); the per-iteration amortized cost isO(1)O(1). Both statements are true. For soft-realtime systems where each tick must finish in bounded time, amortized bounds are not enough — you need worst-case per-step. Most algorithmic problems care only about the aggregate.

Fixed-Size Window: Moving Sums and Averages

The simplest sliding window has a fixed lengthkk. Compute the sum (or any associative aggregate) of A[0..k1]A[0\,..\,k{-}1], then update in O(1)O(1) as the window steps forward:

sumr+1=sumrA[l]+A[r+1]\text{sum}_{r+1} = \text{sum}_r - A[l] + A[r+1]

Total work: one prime of size kk plusnkn - k constant-time updates —O(n)O(n). The naive nested-loop version is O(nk)O(n \cdot k); fork=1000k = 1000 on a 10M-sample audio buffer that is the difference between 10 milliseconds and 10 seconds.

🐍python
1def max_sum_window(arr, k):
2    """Maximum sum of any contiguous length-k subarray. O(n) time."""
3    if k <= 0 or k > len(arr):
4        return None
5    s = sum(arr[:k])               # prime: O(k)
6    best = s
7    for r in range(k, len(arr)):   # slide: O(n - k)
8        s += arr[r] - arr[r - k]   # one add, one subtract
9        best = max(best, s)
10    return best
For moving average just divide the running sum by kk. For moving variance maintain running sum and sum-of-squares; that gives population variance viaσ2=1kxi2μ2\sigma^2 = \frac{1}{k}\sum x_i^2 - \mu^2. For numerically delicate streams use Welford's online update instead — the naive sum-of-squares version loses precision when the values have a nonzero mean and largekk.

Variable-Size Window: Grow Then Shrink

Many problems do not pre-fix the window's length. Instead they ask for the longest (or shortest) window satisfying some predicateP(S)P(S). The pattern is always:

  1. Grow the window on the right by addingA[r]A[r] to the summary.
  2. While the predicate is violated (or, for "at-most" style problems, would be violated), shrink from the left by removing A[l]A[l].
  3. Once the window is valid, record the candidate answer.

For this pattern to work the predicate must be monotone in the window: if a window[l,r][l, r] is invalid, then every superset with the same ll is also invalid (so we have to move ll); equivalently, removing elements cannot turn a valid window into an invalid one (so we never need to rewind). When this monotonicity fails, sliding window does not apply — see the decision section.


Longest Substring Without Repeating Characters

Given a string ss, return the length of its longest substring that contains no repeated characters. A naive scan of every (l,r)(l, r) pair isO(n2)O(n^2) tries times anO(n)O(n) validity check — O(n3)O(n^3). With sliding window plus a last-seen hashmap, O(n)O(n):

Longest substring without repeating characters — O(n) sliding window
🐍longest_no_repeat.py
1Function signature and contract

Take a string s and return the length of the longest substring of s that has no repeated characters. This is the classic LeetCode #3 problem. We will solve it in a single linear pass using a hashmap and two pointers.

2Hashmap of last-seen indices

Maps each character we have ever seen to the most recent index where it appeared. This is what lets us jump l in O(1) when we see a duplicate, instead of re-scanning the window character by character.

EXAMPLE
s = "abcabcbb"  →  last_seen starts as {}
3Initialize the left edge

l is the inclusive left boundary of the current window. Invariant: s[l..r] always contains no duplicates at the end of each iteration.

4Track the best length seen

We only need the LENGTH of the longest no-repeat substring. If you also wanted the substring itself, also store bestL = l at the moment best was updated.

5Outer loop expands the window by one

r is the inclusive right edge. enumerate gives us (index, char) pairs. We advance r exactly n times across the whole call — that is the first n of the 2n total pointer moves the amortized argument promises.

EXAMPLE
iterations on "abcabcbb":
r=0 c=a → r=1 c=b → r=2 c=c → r=3 c=a (DUP!) → r=4 c=b (DUP) → ...
6Detect a duplicate inside the window

Two conditions matter. First, c must be a key in last_seen — if it is not, c is brand new and there is no conflict. Second, last_seen[c] must be ≥ l — otherwise the previous occurrence is OUTSIDE the current window (it has already been left behind by a previous l-jump) and is irrelevant. Both conditions together mean: the duplicate is genuinely inside [l, r-1].

EXAMPLE
r=3, c="a", last_seen["a"]=0, l=0 → 0 ≥ 0 ✓ → duplicate inside window
7Jump l past the duplicate

Set l to (previous index of c) + 1 — the smallest position that excludes the previous c. Crucially we can JUMP, we never walk one step at a time. That is the speedup over a naive two-pointer that increments l by 1 in a loop. l is monotonically non-decreasing — we will never revisit any character.

EXAMPLE
r=3, c="a", last_seen["a"]=0 → l = 1. Window becomes s[1..3] = "bca", clean.
8Record the latest position of c

Always overwrite, even if c was already in the map. We only ever care about the most recent occurrence — older ones can never matter because l has already moved past them (or, after this iteration, will be safely to their right).

EXAMPLE
After r=3: last_seen = {"a":3, "b":1, "c":2}
9Update the running best

Window length is r − l + 1 (because both ends are inclusive). max picks the larger of the previous best and the current length. This single comparison is O(1).

EXAMPLE
r=2, l=0 → length 3, best becomes 3 (window "abc")
r=3, l=1 → length 3, best stays 3
r=4, l=2 → length 3, best stays 3
10Return the answer

Total time is O(n): r moves n times in the for loop, l moves a total of at most n times across all jumps (it never decreases). Space is O(min(n, alphabet size)) for the hashmap — at most 26 entries for lowercase ASCII, at most 1,114,112 for full Unicode in the limit.

EXAMPLE
longest_substring_without_repeats("abcabcbb") = 3  ("abc")
1def longest_substring_without_repeats(s: str) -> int:
2    last_seen = {}                  # char -> most recent index in s
3    l = 0                           # left edge of window (inclusive)
4    best = 0                        # length of best window found so far
5    for r, c in enumerate(s):       # r is the right edge, c = s[r]
6        if c in last_seen and last_seen[c] >= l:
7            l = last_seen[c] + 1    # jump l past the duplicate
8        last_seen[c] = r            # record / overwrite latest position
9        best = max(best, r - l + 1) # update best with current window length
10    return best

Trace on s="abcabcbb"s = \texttt{"abcabcbb"}:

rcactionlwindowbest
0anew0a1
1bnew0ab2
2cnew0abc3
3adup at 0 → l = 11bca3
4bdup at 1 → l = 22cab3
5cdup at 2 → l = 33abc3
6bdup at 4 → l = 55cb3
7bdup at 6 → l = 77b3
Why the last_seen[c]l\texttt{last\_seen}[c] \ge l check matters: a duplicate outside the current window is not a duplicate at all. If we naively jumped ll to last_seen[c]+1\texttt{last\_seen}[c] + 1 in every dup case we would sometimes move ll backwards, breaking the monotonicity that gives usO(n)O(n).

Minimum Window Substring

Given strings ss and tt, return the shortest contiguous substring of ss that contains every character of tt with full multiplicity. The classic FAANG question; a beautiful illustration of the grow-then-shrink pattern with two-tier bookkeeping (a per-character debt vector plus an aggregate "missing" counter).

Minimum window substring — O(n + |alphabet|) sliding window
🐍min_window.py
1Counter for multiset bookkeeping

Counter is dict subclass that defaults missing keys to 0. That convention is essential below: need[c] for a char never seen in t still returns 0, so the > 0 / += 1 logic does not need a separate branch for those cases.

3Function signature

Find the shortest contiguous substring of s that contains every character of t (with multiplicity). Classic LeetCode #76, asked at FAANG-level interviews. Returns the empty string if no such window exists.

4Trivial-input guard

Empty s cannot cover anything; empty t is trivially covered by the empty string. Returning early avoids edge-case bugs in the main loop.

6Build the demand vector

need[c] = how many copies of c we still need to see inside the window. Start it at the full multiplicity from t.

EXAMPLE
t = "ABC" → need = {A:1, B:1, C:1}
t = "AABC" → need = {A:2, B:1, C:1}
7missing = total demand

A single integer that measures the WHOLE shortfall in O(1). When missing reaches 0, the window is valid. We could compute validity by scanning all of need, but that would be O(|alphabet|) per step and ruin our linear time guarantee.

EXAMPLE
t = "ABC" → missing = 3
8Left edge of the window

Inclusive. Will only ever move forward.

9Track the best window

best_l, best_r mark the shortest valid window found so far. best_r = -1 is a sentinel meaning we have not yet found any valid window. We will return the empty string if it stays -1.

11Expand: r walks across s

Same pattern as every sliding window: r advances exactly once per outer iteration, contributing one of the two n-bounded pointer moves.

EXAMPLE
s = "ADOBECODEBANC"  →  r = 0,1,2,...,12
12Is c a useful character?

need[c] > 0 means we still owe at least one copy of c. If c is irrelevant to t (e.g. need[c] = 0 because c never appeared in t, or = 0 because we already have enough copies), this branch is skipped.

EXAMPLE
r=3, c="B", need={A:0, B:1, C:1}, missing=2 → branch taken, missing → 1
13Decrement total shortfall

We just consumed one unit of the genuine demand. Once missing reaches 0, every t-character is fully accounted for — though there may be junk characters between them which the shrink loop will try to remove.

14Decrement debt unconditionally

Always lower need[c] by 1, even for characters that are not in t. This is OK: those characters drop to -1, -2, … and the > 0 test in line 16 / line 19 simply ignores them. This unconditional bookkeeping is what makes the symmetric release logic on lines 19–21 work.

EXAMPLE
r=4, c="E" (not in t): need["E"]: 0 → -1. No effect on missing.
15Inner shrink loop

While the window is valid we shrink l from the left, looking for a smaller valid window. The loop exits as soon as removing one more character would break validity.

16Compare and possibly record

Each shrink iteration tests whether the current window is the best yet. We must record BEFORE we move l, because once we move l we no longer have the current valid window.

EXAMPLE
window = s[0..5] = "ADOBEC", length 6 → first valid → best_len=6, best=(0,5)
17Update best length

Cheap arithmetic; the length is always r − l + 1 (inclusive ends).

18Save the bounds

We only return the substring at the end, so saving (l, r) is enough — no string slicing inside the hot loop.

19Read the character we are about to drop

Save it because we will mutate need[lc] before we examine it again.

20Release the leftmost character

Symmetric to line 14. Adding 1 back to need[lc] reverses the debt change we made when we admitted that occurrence into the window.

EXAMPLE
l=0, lc="A": need["A"]: 0 → 1.
21Did we just break validity?

If, after the increment, need[lc] is positive, then this lc was a NEEDED occurrence — releasing it broke validity. Increment missing to reflect the new shortfall. (If need[lc] is still ≤ 0, lc was a junk character or a redundant copy; window stays valid and the loop continues.)

EXAMPLE
need["A"] becomes 1 (positive) → missing: 0 → 1, loop will exit.
22Advance l

Symmetric to line 11. l also advances at most n times in total across the whole call. Aggregate moves: ≤ 2n. Time: O(n + |alphabet|).

23Slice out the answer

If best_r is still -1 (sentinel), no valid window exists; return empty string. Otherwise, slice s[best_l..best_r] (Python slice end is exclusive, so + 1).

EXAMPLE
s="ADOBECODEBANC", t="ABC" → best=(9,12) → return "BANC"
4 lines without explanation
1from collections import Counter
2
3def min_window_substring(s: str, t: str) -> str:
4    if not s or not t:
5        return ""
6    need = Counter(t)            # required count for each char of t
7    missing = len(t)             # total chars still needed (with multiplicity)
8    l = 0                        # left edge
9    best_l, best_r = 0, -1       # best window so far; -1 means none yet
10    best_len = float("inf")
11    for r, c in enumerate(s):
12        if need[c] > 0:          # c is a useful char (still owed)
13            missing -= 1
14        need[c] -= 1             # debt for c goes down (may go negative)
15        while missing == 0:      # window covers all of t — try to shrink
16            if r - l + 1 < best_len:
17                best_len = r - l + 1
18                best_l, best_r = l, r
19            lc = s[l]
20            need[lc] += 1        # we are about to release s[l]
21            if need[lc] > 0:     # released a char we actually needed
22                missing += 1
23            l += 1
24    return s[best_l:best_r + 1] if best_r >= 0 else ""

Trace on s="ADOBECODEBANC"s = \texttt{"ADOBECODEBANC"},t="ABC"t = \texttt{"ABC"}: the algorithm expandsrr until index 5 (covering oneAA, BB,CC), shrinks to the candidateADOBEC\texttt{ADOBEC} (length 6), keeps scanning, finds another valid window at index 10 (CODEBA\texttt{CODEBA}, also length 6 after shrinking), and finally at r=12r = 12 picks up a fresh CC and shrinks all the way toBANC\texttt{BANC} (length 4). That is the winner.

The two-tier bookkeeping trick. Per-character debts plus an integer missing\texttt{missing} let us check validity in O(1)O(1). Withoutmissing\texttt{missing} we would need to scan the debt dictionary every step — correct, butO(nΣ)O(n \cdot |\Sigma|) instead ofO(n+Σ)O(n + |\Sigma|). For 1M-character DNA sequences with the alphabet{A,C,G,T}\{A, C, G, T\} the difference is small; for full Unicode it is enormous.

Longest Subarray With Sum at Most K

Given an array of positive integersAA and an integerKK, return the length of the longest contiguous subarray whose sum is at mostKK. Grow on the right; while the running sum exceeds KK, shrink on the left:

🐍python
1def longest_sum_at_most(arr, K):
2    """Longest contiguous window with sum <= K. arr must be NON-NEGATIVE."""
3    s, l, best = 0, 0, 0
4    for r in range(len(arr)):
5        s += arr[r]                  # add right
6        while s > K and l <= r:      # shrink while invalid
7            s -= arr[l]
8            l += 1
9        best = max(best, r - l + 1)  # record
10    return best

Why positivity matters

If AA can contain negative numbers, adding A[r]A[r] can decrease the sum, and dropping A[l]A[l] can increase it. Validity is no longer monotone in the window: a window can become valid, then invalid, then valid again as we walk. The sliding-window approach silently produces wrong answers. Use a prefix-sum + balanced-BST or prefix-sum + monotonic-deque approach instead (covered in Ch 7 with monotonic stacks). This restriction is the single most common source of bugs when a real-world array unexpectedly contains zero or negative samples (e.g. AC-coupled sensor data).

Sliding Window Maximum (Monotonic Deque)

Given AA and a window sizekk, output an arrayMM with M[i]=max(A[i..i+k1])M[i] = \max(A[i \,..\, i+k-1]) fori=0,,nki = 0, \dots, n-k. Naive: per-window scan of kk elements,O(nk)O(n \cdot k). A heap solution shaves it toO(nlogk)O(n \log k) but requires lazy deletion. A monotonic deque nails it atO(n)O(n).

The deque stores indices whose values are strictly decreasing. The head is therefore always the argmax of the current window. Two invariants maintain this:

  1. When a new index rr arrives, pop tail indices whose values are A[r]\le A[r]: they are dominated by a younger, larger element and can never be a future maximum.
  2. When the window has slid past the head index (headrk\le r - k), evict it.
Sliding window maximum in TypeScript — O(n) monotonic deque
📘slidingWindowMax.ts
1Function signature

Given an integer array nums and a window size k, return an array out where out[i] is the maximum of nums[i..i+k-1]. Naive solution: scan k elements per output, O(n·k). We will hit O(n) using a monotonic deque.

2Deque of INDICES (not values)

We store indices because we need to know when an index has 'fallen off' the left edge of the window. The values nums[dq[0]], nums[dq[1]], …, nums[dq[back]] form a strictly decreasing sequence. The head dq[0] is therefore always the index of the maximum of the current window.

EXAMPLE
nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3:
at r=2 the deque holds [1, 2] → values [3, -1] (decreasing).
3Output array

Will have length n − k + 1. We push one entry every time we have a full window, i.e. once r ≥ k − 1.

4Right pointer advances exactly n times

This loop is the source of one of the two n-bounded pointer movements. Inside the loop we will pop from the deque, but every index is pushed at most once and popped at most once, so the AGGREGATE work inside is O(n), not O(n) per iteration.

6Expire old indices

If the deque head is at index ≤ r − k, it has fallen out of the current window [r − k + 1, r] and must be evicted. This is a while loop only for code symmetry — only one element can ever be at the very head, so this loop runs at most once per outer iteration.

EXAMPLE
k=3, r=3 → window is [1..3]. If dq[0]=0, then 0 ≤ 3 − 3 = 0 ✓ → shift it out.
8Maintain the decreasing invariant

Before pushing r, drop every tail index whose VALUE is ≤ nums[r]. Reason: those indices can never be the maximum of any future window that also contains r, because nums[r] is at least as large AND r is younger (will live longer). Each index is popped here at most once over the entire algorithm — that is why the inner while loops aggregate to O(n).

EXAMPLE
nums=[1,3,-1,...], k=3:
at r=1, nums[r]=3, deque had [0] with nums[0]=1.
1 ≤ 3 → pop. Then push r=1. deque=[1].
10Push current index

After the eviction loops, the back invariant still holds (every value before is strictly greater than nums[r]). Push r.

12Emit a result once the window is full

For the first k − 1 iterations the window is still being primed; do not emit. From r = k − 1 onward the head of dq is exactly the index of the maximum of nums[r − k + 1..r].

EXAMPLE
nums=[1,3,-1,-3,5,3,6,7], k=3:
r=2 → dq=[1,2] (values 3,-1) → push 3.
r=3 → dq=[1,2,3] (3,-1,-3) → push 3.
r=4 → all popped, dq=[4] (5) → push 5. ...
14Return the array

Total time O(n), total space O(k) for the deque plus O(n − k + 1) for the output. Each index is pushed at most once and popped at most once: that is the amortized analysis of monotonic deques in one sentence.

6 lines without explanation
1function slidingWindowMax(nums: number[], k: number): number[] {
2  const dq: number[] = [];           // indices, values strictly decreasing
3  const out: number[] = [];
4  for (let r = 0; r < nums.length; r++) {
5    // 1. Drop indices that have fallen off the left of the window.
6    while (dq.length > 0 && dq[0] <= r - k) dq.shift();
7    // 2. Drop tail indices whose values are <= nums[r] (they can never win).
8    while (dq.length > 0 && nums[dq[dq.length - 1]] <= nums[r]) dq.pop();
9    // 3. Append current index.
10    dq.push(r);
11    // 4. Once we have a full window, the head of dq is its maximum.
12    if (r >= k - 1) out.push(nums[dq[0]]);
13  }
14  return out;
15}

Trace on A=[1,3,1,3,5,3,6,7]A = [1, 3, -1, -3, 5, 3, 6, 7] with k=3k = 3:

rA[r]deque (indices)deque (values)emit
01[0][1]
13[1][3]
2-1[1, 2][3, -1]3
3-3[1, 2, 3][3, -1, -3]3
45[4][5]5
53[4, 5][5, 3]5
66[6][6]6
77[7][7]7

Output: [3,3,5,5,6,7][3, 3, 5, 5, 6, 7]. Each index is pushed once and popped at most once across the whole run — the total work for the inner while loops is bounded bynn, so the algorithm isO(n)O(n) in aggregate.

Replace \le with<< in the eviction loop and you get a non-strict monotone deque that keeps duplicates — useful if you need stable tie-breaking. Replace nums[r] by -nums[r] and you get sliding-window minimum.

Permutation in a String

Does s2s_2 contain any permutation ofs1s_1 as a contiguous substring? Equivalent to: is there an ll such that the character frequency of s2[l..l+s11]s_2[l\,..\,l + |s_1| - 1] equals the frequency of s1s_1? Fixed-size window of length s1|s_1|; maintain a per-character difference vectorD[c]=freqwin(c)freqs1(c)D[c] = \text{freq}_{\text{win}}(c) - \text{freq}_{s_1}(c). The window is a match exactly whenD0D \equiv 0. To avoid scanningDD every step, also maintain a countermatches\texttt{matches} = number of characters for which D[c]=0D[c] = 0. Each window step changes DD in two slots, somatches\texttt{matches} updates inO(1)O(1). The answer is yes iffmatches=Σ\texttt{matches} = |\Sigma| at any step.

🐍python
1def has_permutation(s1, s2):
2    if len(s1) > len(s2):
3        return False
4    need = [0] * 26
5    have = [0] * 26
6    for c in s1:
7        need[ord(c) - ord('a')] += 1
8    matches = sum(1 for i in range(26) if need[i] == have[i])
9    for r, c in enumerate(s2):
10        i = ord(c) - ord('a')
11        have[i] += 1
12        if have[i] == need[i]:
13            matches += 1
14        elif have[i] - 1 == need[i]:   # we just unmatched
15            matches -= 1
16        if r >= len(s1):                # window now too big — shrink left
17            j = ord(s2[r - len(s1)]) - ord('a')
18            have[j] -= 1
19            if have[j] == need[j]:
20                matches += 1
21            elif have[j] + 1 == need[j]:
22                matches -= 1
23        if matches == 26:
24            return True
25    return False
Time O(s2+Σ)O(|s_2| + |\Sigma|), spaceO(Σ)O(|\Sigma|). The technique generalizes to any equality-of-multisets test over a sliding window — a building block for plagiarism detection (rolling shingles), DNA motif scanning, and rolling-hash duplicate detection (preview Rabin-Karp in Ch 9).

The Universal Sliding-Window Template

Every sliding-window problem in this section is an instance of the same loop. Memorize this skeleton; the only thing that changes between problems is the choice of summarySS and the predicatevalid\texttt{valid}.

🐍python
1def sliding_window_template(A):
2    l = 0
3    S = init_state()           # e.g. running sum, Counter, deque, ...
4    best = init_best()
5    for r in range(len(A)):
6        S = add(S, A[r])       # extend window to include A[r]
7        while not valid(S):    # tighten until invariant holds
8            S = remove(S, A[l])
9            l += 1
10        best = update(best, l, r, S)
11    return best
  1. add / remove must be O(1) amortized for the whole template to be O(n)O(n). Hashmap counts, running sums, and monotonic deques satisfy this; sorted-set insertion (O(logn)O(\log n)) bumps the total toO(nlogn)O(n \log n), which is still fine.
  2. valid must be monotone: if removing elements from a valid window keeps it valid (or vacuously so), we are safe to shrink without rewinding.
  3. update is O(1): read off the current best fromrl+1r - l + 1 and / orSS.
  4. For at-most problems (longest sub-window with sum ≤ K, with at most k distinct, etc.) shrink whilenot valid\texttt{not valid} and record after the inner loop. For at-least problems (shortest sub-window covering all of t, etc.) shrink whilevalid\texttt{valid} and record inside the inner loop.

Interactive: The Sliding Window Lab

Pick a problem, edit the inputs, and step the window forward manually or hit Run for auto-play. Green cells are the current window; the blue l arrow and red r arrow mark the boundaries. Watch the "Internal state" panel update as SS changes — you can literally see the algorithm's memory.

The Sliding Window Lab

02l1125r314352647681
step 1 / 8
What just happened
Prime the window: sum = 8
Internal state
sum = 2 + 1 + 5 = 8
Best answer so far
8 at l=0

Green cells are inside the current window. Blue l and red r mark the window boundaries (both inclusive). Step forward to watch each pointer advance — notice they only ever move right, never left. That is what gives sliding window its linear-time guarantee.

Try this experiment on the "Min window substring" mode. Set s = "XYXYZAB" and t = "XYZ". Watch how the window grows to XYXYZ, becomes valid, and the inner shrink loop fires twice (dropping the leading X and Y) to land on XYZ. The two phases — expand right, then collapse left — are visible in real time.

Quick Check

On the array A = [1, 3, -1, -3, 5, 3, 6, 7] with k = 3, the sliding window MAXIMUM is...

Quick Check

Which of these problems can be solved by a vanilla sliding window in O(n)?


Real-World Applications

Sliding windows are everywhere outside of interview questions; the technique is the algorithmic fabric beneath whole engineering domains:

DomainWindow contentWhat is computed
Audio DSPk consecutive samplesMoving average for a low-pass filter; rolling RMS for a VU meter; rolling autocorrelation for pitch tracking.
Computer visionk×k pixel patchConvolution kernel: every output pixel is a weighted sum of a sliding patch — the workhorse beneath every CNN.
Network monitoringTimestamps in the last T secondsToken-bucket / leaky-bucket rate limiters; SYN-flood detection; per-source request quotas.
Stock tradingLast n closing pricesSimple moving averages, Bollinger Bands (mean ± k·σ over the window), MACD crossovers.
GenomicsAll length-k subwords (k-mers)GC content scanning, motif discovery, minimizer-based sequence alignment in BWA / minimap2.
Search enginesRolling hash of a 5-shingle of wordsNear-duplicate document detection; Rabin–Karp substring search.
Operating systemsLast N system calls per processAnomaly detection, sequence-of-calls intrusion detection.
Real-time analyticsEvents in last 5 minTumbling vs sliding vs hopping windows in Kafka Streams, Flink, ksqlDB.
Trading order booksTrades in last 30sTime-windowed VWAP, microstructure metrics, rolling order-book imbalance.
SRE / observabilityLast 1000 eventsp50/p95/p99 latency trackers using approximate sketches over a rolling window.

Streams change the rules

For an unbounded stream you cannot keep every element in the window if the window is huge or the stream is fast. Approximate sliding-window algorithms (count-min sketch, HyperLogLog, t-digest, DDSketch) trade exactness for sub-linear memory. We cover those in Ch 25 (probabilistic data structures). The exact technique in this section is the foundation; the streaming versions are memory-bounded approximations of the same idea.

Pitfalls and Common Bugs

  • Inclusive vs exclusive bounds. Decide once, at the top: is the window A[l..r]A[l\,..\,r] (both inclusive, length rl+1r - l + 1) orA[l..r)A[l\,..\,r) (right-exclusive, length rlr - l)? Mixing the two within one function is a classic off-by-one factory.
  • Forgetting to tighten. If your while-loop is if instead of while, the window can contract by only one step per iteration and you may miss valid configurations — or worse, leave the invariant violated and silently emit a wrong answer.
  • Hashmap leftovers. When the window slides off a character, decrement (or delete) its entry in the summary. Stale counts produce ghosts — the dictionary remembers characters that are no longer in the window and the predicate fires falsely.
  • Negative or zero values in "sum at most K" or "product at most K" problems break monotonicity. Check the input domain explicitly. AC-coupled sensor data, log-returns on stock prices, and signed deltas are common real-world instances where this bites.
  • Empty-window edge cases. Ifrr can fall behindll (it should not, in the canonical template), your add / remove logic must handle the empty window cleanly. Most bugs here come from initializingrr before the first iteration and then double-counting on the first add.
  • Counting answers vs lengths. "Number of subarrays with sum at most K" is the cumulative sum ofrl+1r - l + 1 over allrr — not just the maximum. Easy to confuse with "longest subarray with sum at most K."

When Sliding Window Does Not Apply

Sliding window's linear-time guarantee depends on the non-rewinding property of ll. When validity is not monotone, that property fails. The replacement-of-choice depends on which monotonicity is violated:

ProblemMonotonicity issueReplacement
Subarray with sum exactly K (signed)Adding / removing can jump validity in either directionPrefix sum + hashmap of seen prefix sums (Ch 9)
Subarray with sum at most K (signed)Removing a negative can break validityPrefix sum + monotonic deque or balanced BST
k-th smallest in window (general)Smallest is not preserved by add / remove without re-sortingOrder-statistic tree, two heaps, or wavelet tree
Median in windowSame as aboveTwo heaps with lazy deletion, or order-statistic tree
Mode (most frequent) in windowNo O(1) update for the argmax of a multiset under add / removeWavelet tree or sqrt-decomposition (advanced)
All windows with sum exactly K (positives only)Monotone — sliding window does work hereVanilla sliding window: shrink while sum > K, count when sum == K
The decision protocol when you spot a window-shaped problem:
  1. Identify the summary SS and predicate P(S)P(S).
  2. Check: if P([l,r])P([l, r]) holds, doesP([l+1,r])P([l+1, r]) hold? (Monotone shrinkage.)
  3. Check: are add / remove bothO(1)O(1) amortized?
  4. If both yes ⇒ sliding window. Plug into the template.
  5. If no ⇒ reach for prefix sums + hashmap, monotonic deques, balanced BSTs, or two-heap medians. Each pattern has a chapter ahead.

Sliding window is the first "design pattern" of array algorithms: a small, reusable mental machine that turnsO(n2)O(n^2) brute force into cleanO(n)O(n) code. Everything else in this chapter — two pointers, prefix sums, dynamic arrays — will keep cross-referencing it. Master the template now, and three chapters from now monotonic stacks and deques (Ch 7) will feel like minor variations on this same theme.

Loading comments...