Part I · Arrays, Strings and Pointers Pattern 1 4 problems

Sliding Window

Turn a nested loop over every subarray into a single pass, by never recomputing what you already know.

The sliding window is the first pattern worth learning because it converts the most common brute force in interviews, check every subarray, from quadratic to linear. The brute force is O(n²) or O(n²·k). The window is O(n). The gap is the whole question.

Contents

  1. When to use
  2. Core idea
  3. The two templates
  4. Common mistakes
  5. Maximum Subarray of Size K
  6. Longest Substring Without Repeating Characters
  7. Minimum Size Subarray Sum
  8. Minimum Window Substring
  9. Recap

When to use

The trigger. The input is a linear structure, an array, a string or a linked list. You are asked for the longest, shortest, or best contiguous run that meets some condition. The word to listen for is contiguous: subarray, substring, window, or a run of k in a row.

Three signals put together tell you it is a window problem.

SignalWhat it looks like in the prompt
Contiguity“subarray”, “substring”, “consecutive”, “in a row”. Not “subsequence”.
An extremum“longest”, “shortest”, “maximum sum”, “minimum length”, “count how many”.
A monotone conditionGrowing the window can only make the condition harder, shrinking it can only make it easier. Sums of non-negative numbers, distinct-character counts, and “at most k of X” all behave this way.

When not to use it

The negative-number trap. “Smallest subarray with sum at least target” is a clean window problem when all values are positive. Put one negative number in the array and the window breaks, because total is no longer monotone in the window width. Always ask the interviewer whether values can be negative. Asking earns points; assuming loses them.

Core idea

Keep two indices, left and right, marking a half-open run s[left..right]. Keep alongside them a small summary of what is inside: a running sum, a character count, a number of distinct values. Move right forward to grow, move left forward to shrink. Each move updates the summary in O(1). Nothing is ever recomputed from scratch.

The brute force recomputes the summary of each candidate run from its own elements, so an element is touched once per window that contains it. The window instead updates the summary incrementally: one addition when an element enters, one subtraction when it leaves.

step t 2 1 5 1 3 2 left right window sum = 7 slide one step step t+1 2 1 5 1 3 2 7 − 1 + 3 = 9 two arithmetic operations, not three
Figure 1.1 — A fixed window of width 3. Sliding costs one subtraction and one addition, whatever the window width.

Why the total work is linear

The inner while loop that shrinks the window looks like it might make the algorithm quadratic. It does not, and the reason is worth being able to state cleanly in an interview.

The amortised argument. left and right each only ever increase, and neither can exceed n. So across the whole run there are at most n increments of right and at most n increments of left, giving at most 2n pointer moves in total. Each move does O(1) work. Total: O(n), no matter how the inner loop is distributed.

Put another way: every element enters the window exactly once and leaves it at most once. Two touches per element.

The invariant

Every window problem has one sentence that is true at the top of each iteration. Write it as a comment. It is how you convince yourself, and the interviewer, that the code is right.

The two templates

Nearly every window problem is one of these two. Learn both by heart.

Template A — fixed width k
def fixed_window(nums: list[int], k: int) -> int:
    """Best value over every window of exactly k elements."""
    window = sum(nums[:k])           # pay for the first window once
    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

Use when the prompt names the width. There is no left variable, because left is always right - k + 1.

Template B — variable width
def variable_window(items: list[int]) -> int:
    """Longest (or shortest) window satisfying some condition."""
    left = 0
    state = 0                        # the running summary of the window
    best = 0

    for right, value in enumerate(items):
        state += value               # 1. grow: value enters on the right

        while is_invalid(state):     # 2. shrink until the window is legal again
            state -= items[left]
            left += 1

        best = max(best, right - left + 1)   # 3. record

    return best

Three beats: grow, shrink, record. For a longest answer you record after the shrink loop. For a shortest answer you record inside the shrink loop, because each shrink step gives a smaller valid window.

The one rule that decides where best goes. Ask what the while condition means. If the loop runs while the window is illegal, the window is legal only after the loop, so record after. If the loop runs while the window is legal (you are shrinking a valid window to find the tightest one), record inside.

Common mistakes

The problems

1. Maximum Subarray of Size K Easy

Problem

Given an array of integers nums and an integer k, return the maximum sum of any contiguous subarray of length exactly k.

Approach

Solution

def max_subarray_sum_of_size_k(nums: list[int], k: int) -> int:
    """Return the largest sum of any contiguous subarray of length k.

    Args:
        nums: Input numbers. Values may be negative.
        k: Window length. Must satisfy 1 <= k <= len(nums).

    Returns:
        The maximum window sum.

    Raises:
        ValueError: If k is outside 1..len(nums).

    Example:
        >>> max_subarray_sum_of_size_k([2, 1, 5, 1, 3, 2], 3)
        9
    """
    if not 1 <= k <= len(nums):
        raise ValueError(f"k must be in 1..{len(nums)}, got {k}")

    # Invariant: window_sum == sum(nums[right - k + 1 : right + 1])
    window_sum = sum(nums[:k])
    best = window_sum

    for right in range(k, len(nums)):
        window_sum += nums[right] - nums[right - k]
        best = max(best, window_sum)

    return best

Walkthrough

With nums = [2, 1, 5, 1, 3, 2] and k = 3:

rightentersleaveswindow_sumbest
2, 1, 588
31278
43199
52569
TimeO(n)SpaceO(1)

Edge cases to raise

Say this out loud: “Neighbouring windows share k - 1 elements, so I only need the difference between them, which makes each step constant time.”

2. Longest Substring Without Repeating Characters Medium

Problem

Given a string s, return the length of the longest substring with no repeated characters. For "abcabcbb" the answer is 3, from "abc".

Approach

Solution

def length_of_longest_substring(s: str) -> int:
    """Length of the longest substring of s with all-distinct characters.

    Args:
        s: Input string. May be empty.

    Returns:
        The length of the longest run with no repeats. 0 for an empty string.

    Example:
        >>> length_of_longest_substring("pwwkew")
        3
    """
    last_seen: dict[str, int] = {}   # character -> index of its last occurrence
    left = 0                         # first index still inside the window
    best = 0

    for right, ch in enumerate(s):
        # Only a repeat *inside* the current window forces a jump.
        if ch in last_seen and last_seen[ch] >= left:
            left = last_seen[ch] + 1

        last_seen[ch] = right
        best = max(best, right - left + 1)

    return best

Why the >= left check matters

The dictionary remembers characters that have already fallen out of the window. Take "abba". At right = 3 the character 'a' was last seen at index 0, but left is already 2, so 'a' is not in the window and no jump is needed. Without the guard, left would move backwards to 1 and the answer would be wrong. This one comparison is what the interviewer is watching for.

Walkthrough

s = "abba":

rightchjump?leftwindowbest
0ano0"a"1
1bno0"ab"2
2byes, to 22"b"2
3ano, 0 < left2"ba"2
TimeO(n)SpaceO(min(n, σ))σalphabet size

Edge cases to raise

Say this out loud: “When I hit a repeat I do not shrink one step at a time, I jump left past the previous occurrence, because every start before that point is already invalid.”

3. Minimum Size Subarray Sum Medium

Problem

Given an array of positive integers nums and a positive integer target, return the minimal length of a contiguous subarray whose sum is at least target. Return 0 if no such subarray exists.

Approach

Solution

import math


def min_subarray_len(target: int, nums: list[int]) -> int:
    """Shortest contiguous subarray of nums with sum >= target.

    Assumes every value in nums is positive, which is what makes the
    running total monotone in the window width and the shrink step valid.

    Args:
        target: Required minimum sum, positive.
        nums: Positive integers.

    Returns:
        The length of the shortest qualifying subarray, or 0 if there is none.

    Example:
        >>> min_subarray_len(7, [2, 3, 1, 2, 4, 3])
        2
    """
    left = 0
    total = 0
    best = math.inf

    for right, value in enumerate(nums):
        total += value

        # The window is valid; squeeze it from the left while it stays valid.
        while total >= target:
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1

    return 0 if best == math.inf else int(best)

Walkthrough

target = 7, nums = [2, 3, 1, 2, 4, 3]:

righttotal after addshrink stepsbest
0..22, 5, 6none, below target
38drop 2 → 6, left=14
410drop 3 → 7, drop 1 → 6, left=33 then 2
59drop 2 → 7, drop 4 → 3, left=52

The answer is 2, from [4, 3].

TimeO(n)SpaceO(1)

The follow-up you will be asked

“What if the numbers can be negative?” The window collapses, because adding an element can lower the total, so “shrink while valid” is no longer safe. Switch to prefix sums plus a monotonic deque, which gives O(n), or prefix sums in a sorted structure for O(n log n). Naming the failure and the replacement is the answer they want.
There is also an O(n log n) solution for the positive case: build the prefix-sum array, which is strictly increasing, then for each end index binary-search for the earliest start whose prefix is small enough. Worth mentioning as a second approach, but the window is strictly better.

Edge cases to raise

Say this out loud: “Because all values are positive the total only grows as the window widens, so the first time it crosses the target I can shrink greedily and stay correct.”

4. Minimum Window Substring Hard

Problem

Given strings s and t, return the shortest substring of s that contains every character of t, counting repeats. Return "" if there is none. For s = "ADOBECODEBANC" and t = "ABC" the answer is "BANC".

Approach

Solution

import math
from collections import Counter


def min_window(s: str, t: str) -> str:
    """Shortest substring of s containing every character of t, with repeats.

    Args:
        s: The string to search.
        t: The required characters, with multiplicity.

    Returns:
        The shortest qualifying substring, or "" if none exists.

    Example:
        >>> min_window("ADOBECODEBANC", "ABC")
        'BANC'
    """
    if not s or not t or len(t) > len(s):
        return ""

    need: Counter[str] = Counter(t)   # >0 means still owed, <0 means surplus
    missing = len(t)                  # required characters not yet covered
    best_start, best_len = 0, math.inf
    left = 0

    for right, ch in enumerate(s):
        # Grow. A positive need means this character actually pays down a debt.
        if need[ch] > 0:
            missing -= 1
        need[ch] -= 1

        # Valid window. Shrink from the left while it stays valid.
        while missing == 0:
            if right - left + 1 < best_len:
                best_start, best_len = left, right - left + 1

            leaving = s[left]
            need[leaving] += 1
            if need[leaving] > 0:     # we just gave back a character we needed
                missing += 1
            left += 1

    return "" if best_len == math.inf else s[best_start : best_start + best_len]

Why the counter is allowed to go negative

This is the subtle part, and the reason the code is short. Suppose t = "AB" and the window is "AAB". Then need['A'] == -1. When the leftmost 'A' leaves, need['A'] becomes 0, which is not positive, so missing stays at 0 and the window is still valid, correctly. Only when a genuinely needed character leaves does need rise above zero and missing tick back up. The sign of the counter encodes surplus versus debt for free.

Walkthrough

s = "ADOBECODEBANC", t = "ABC". The window first becomes valid at "ADOBEC", length 6. Shrinking is blocked immediately, because dropping 'A' breaks it. Later the window "CODEBA" is valid at length 6, then "BANC" at length 4, which is the answer.

First valid windowLengthbest
ADOBEC6ADOBEC
CODEBA6ADOBEC
BANC4BANC
TimeO(|s| + |t|)SpaceO(|t|)
On the space bound. need also picks up entries for characters of s that are not in t, so strictly it is O(|s| + |t|) keys in the worst case. Guarding with if ch in need before touching the counter keeps it at O(|t|) and is worth a sentence if the interviewer pushes on memory.

Edge cases to raise

Say this out loud: “I keep one integer, missing, so checking whether the window is valid is O(1) instead of comparing two dictionaries. The counter goes negative to record surplus characters, which is what makes the shrink step correct.”

Recap

The six things to carry forward

Where this goes next

Pattern 2, Two Pointers, keeps the two-index idea but changes the motion: instead of both pointers walking forward, they start at opposite ends and converge. That small change unlocks sorted-array problems the window cannot touch.


All 12 patterns 2 — Two Pointers