Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 5 – Sliding window

Sliding windows: fixed and variable

Recognise sliding-window problems, write the fixed and variable templates, solve the wettest week of a rain log in full and see where windows fail.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free: 25 minutes

What you will learn

  • Recognise problems where a contiguous window can grow and shrink monotonically
  • Write fixed-size and variable-size window templates and state the invariant that makes each one correct
  • Detect when a window fails (values below zero, rules that are not monotone) and switch to prefix sums

Before you start

What you’ll learn · Before you start

What you will learn

  • Recognise problems where a contiguous window can grow and shrink monotonically
  • Write fixed-size and variable-size window templates and state the invariant that makes each one correct
  • Detect when a window fails (values below zero, rules that are not monotone) and switch to prefix sums

Before you start

On this page

A sliding window is a stretch of consecutive items in an array or a string, marked by two indices, left and right, that only ever move forward. Instead of adding up or recounting every stretch from scratch, you keep one running summary of what is inside the window, such as its total, and update it as an item enters on the right or leaves on the left. A fixed window always holds k items, so every step adds one item and removes one. A variable window grows on the right and shrinks on the left whenever it breaks a rule, such as “the total is at most 20”. Either way, a scan that looked at every stretch, O(n·k) or O(n²), becomes O(n). This lesson solves one problem completely with a fixed window, in Python, JavaScript and C++, then builds the variable window and shows a kind of input that breaks it: values below zero.

When a window fits the problem

A window is two pointers that move the same way, like the reader–writer pair of the two-pointers overview, except that what matters here is the stretch between them. Two things have to be true before a window can help.

  1. The answer concerns a contiguous stretch. The wording usually says so: “consecutive days”, “a substring”, “a subarray”, “a run of”, “within any 10 minutes”. A subsequence may skip items, so it is not a window, and neither is “any two items” (a job for hashing or two pointers) or “any subset”.
  2. The summary changes by a small, exact step when one item enters or leaves. A total, a count, a table of letter counts and the number of distinct values in such a table all do. The largest value does not: when the maximum leaves the window, nothing in the old summary says what the new maximum is. Windows that need the maximum keep a queue of candidate positions instead, a monotonic deque.

A variable window needs a third property, monotone validity: if a window obeys the rule, every shorter window inside it obeys it too. “At most 20 mm of rain” behaves like that, because rainfall is never negative, and so do “no repeated letter” and “at most k distinct values”. Questions about the shortest window turn it round: if a window obeys the rule (“at least 100 mm”, “contains every required letter”), every longer window that contains it obeys it too. Either form is what lets the window throw items away on the left and never look at them again.

Fixed or variable window?
Criterion FixedVariable
Size Always k itemsGrows and shrinks
Each step One in, one outOne in; shrink until valid
Needs An updatable summaryPlus monotone validity
Values below zero Still correctBreaks rules on totals
Cost O(n)O(n) amortised
When to choose The length is givenThe length is unknown

A problem solved in full: the wettest week

The problem

A rain gauge at a hill station logs the rainfall of each day in whole millimetres. Given the log rain of n days and a number k, return the highest total over any k consecutive days and the first day of that stretch, counting days from 0. If several stretches tie, return the earliest. Limits: 1 ≤ k ≤ n ≤ 100,000 and 0 ≤ rain[i] ≤ 500.

For the log [12, 0, 35, 4, 0, 61, 8, 17, 0, 3] and k = 3 the answer is (86, 5): days 5, 6 and 7 hold 61 + 8 + 17 = 86 mm. With k = 1 it is the wettest single day, (61, 5), and with k = 10 the whole log, (140, 0).

The brute force, and why it is too slow

Add up each of the n − k + 1 stretches on its own and keep the largest. It is short and plainly right, and it does (n − k + 1) × k additions. That is worst near k = n / 2: at n = 100,000 and k = 50,000 it is 50,001 × 50,000, about 2.5 billion additions. A common planning figure is 10⁸ simple steps a second for compiled code and 10⁷ for CPython (the lesson on constraints explains where it comes from), so even compiled code would need tens of seconds, far beyond a time limit of a few seconds.

What to notice

Two neighbouring stretches share k − 1 days. Moving from days 4–6 to days 5–7 removes day 4 and adds day 7, and nothing else changes:

A window of 3 days moves one day on: day 4 (0 mm) leaves, day 7 (17 mm) enters, and the total goes from 69 mm to 86 mm.Days 4–6: 0 + 61 + 8 = 69 mmDays 5–7: 69 − 0 + 17 = 86 mmday 40day 561day 68day 717day 40leavesday 561day 68day 717enterssubtract the day that leaves,add the day that enters

One step of a fixed window of 3 days

Text description of the diagram

The diagram shows days 4 to 7 of the sample log twice, one row above the other. The rainfall of those days is 0, 61, 8 and 17 mm.

  1. In the top row the window covers days 4, 5 and 6, drawn with thick borders. Its total is 0 + 61 + 8 = 69 mm.
  2. In the bottom row the window has moved one day on and covers days 5, 6 and 7. Day 4, drawn dashed, has left the window, and day 7 has entered it.

The arrow between the rows says how the new total is found: subtract the day that leaves and add the day that enters, 69 − 0 + 17 = 86 mm, without adding up the three days again.

So no stretch needs adding up twice. Keep one total, and at each step add the day that enters and subtract the day that leaves.

The window, and why it is right

The loop keeps one statement true, its invariant: after the step for day right, total is the sum of the k days that end at right, and best_total is the largest such sum so far, with best_start the earliest first day that reaches it.

  • Before the loop the first k days are added up directly, so the statement holds for right = k − 1.
  • Each step adds rain[right] and subtracts rain[right - k]. That turns the sum of days right − k to right − 1 into the sum of days right − k + 1 to right, which is exactly the next window.
  • Ties go to the earliest stretch, because best_total changes only when a total is strictly larger.
  • At the end every one of the n − k + 1 windows has been compared once, so best_total is the answer.

The work is k additions for the first window and then two per step, k + 2(n − k) in all, which is at most 2n: O(n) time and O(1) extra memory, whatever k is. The lesson on dry runs and invariants shows how to state and check an invariant like this one in a round.

Code in Python and JavaScript

The wettest k days: brute force and window

Python · week/wettest_week.py

"""The wettest k consecutive days of a rain log, two ways: a brute force and a fixed-size sliding window."""


def wettest_brute(rain, k):
    """Add up every stretch of k days from scratch. Returns (total, first day of the stretch)."""
    totals = [sum(rain[start:start + k]) for start in range(len(rain) - k + 1)]
    best_total = max(totals)
    return best_total, totals.index(best_total)  # index() finds the earliest stretch with that total


def wettest_window(rain, k):
    """Slide one window of k days along the log: add the day that enters, subtract the day that leaves."""
    total = sum(rain[:k])  # the first window: days 0 to k - 1
    best_total, best_start = total, 0
    for right in range(k, len(rain)):
        total += rain[right] - rain[right - k]  # day `right` enters, day `right - k` leaves
        if total > best_total:  # strictly greater, so the earliest stretch wins a tie
            best_total, best_start = total, right - k + 1
    return best_total, best_start


def season(days, seed):
    """A made-up rain log in whole millimetres, the same on every computer: multiply by 48271 and keep the remainder
    modulo 2**31 - 1, the random generator that C++ calls std::minstd_rand."""
    state, rain = seed, []
    for _ in range(days):
        state = state * 48271 % 2_147_483_647
        rain.append(0 if state % 5 < 2 else 1 + state // 5 % 90)  # about two days in five stay dry
    return rain


sample = [12, 0, 35, 4, 0, 61, 8, 17, 0, 3]
log = season(122, seed=7)
print("first days of the season:", log[:10])
print("log       k  brute force  window")
for name, rain, k in [("sample", sample, 3), ("sample", sample, 1), ("sample", sample, 10), ("season", log, 7)]:
    # a typical k, the smallest, k = n (a single window), and a week of the made-up season
    print(f"{name:<8}{k:>3}  {str(wettest_brute(rain, k)):<11}  {wettest_window(rain, k)}")

Output

first days of the season: [80, 52, 33, 86, 0, 7, 0, 0, 0, 87]
log       k  brute force  window
sample    3  (86, 5)      (86, 5)
sample    1  (61, 5)      (61, 5)
sample   10  (140, 0)     (140, 0)
season    7  (362, 82)    (362, 82)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 wettest_week.py

JavaScript · week/wettest_week.mjs

// The wettest k consecutive days of a rain log, two ways: a brute force and a fixed-size sliding window.

// Add up every stretch of k days from scratch. Returns [total, first day of the stretch].
function wettestBrute(rain, k) {
  const totals = [];
  for (let start = 0; start + k <= rain.length; start++) {
    totals.push(rain.slice(start, start + k).reduce((sum, mm) => sum + mm, 0));
  }
  const bestTotal = totals.reduce((a, b) => Math.max(a, b)); // not Math.max(...totals): long arrays overflow the call
  return [bestTotal, totals.indexOf(bestTotal)]; // indexOf() finds the earliest stretch with that total
}

// Slide one window of k days along the log: add the day that enters, subtract the day that leaves.
function wettestWindow(rain, k) {
  let total = 0;
  for (let day = 0; day < k; day++) total += rain[day]; // the first window: days 0 to k - 1
  let bestTotal = total;
  let bestStart = 0;
  for (let right = k; right < rain.length; right++) {
    total += rain[right] - rain[right - k]; // day `right` enters, day `right - k` leaves
    if (total > bestTotal) {
      // strictly greater, so the earliest stretch wins a tie
      bestTotal = total;
      bestStart = right - k + 1;
    }
  }
  return [bestTotal, bestStart];
}

// A made-up rain log in whole millimetres, the same on every computer: multiply by 48271 and keep the remainder
// modulo 2 ** 31 - 1, the random generator that C++ calls std::minstd_rand. Every product stays below 2 ** 53, so
// plain numbers keep it exact.
function season(days, seed) {
  let state = seed;
  const rain = [];
  for (let i = 0; i < days; i++) {
    state = (state * 48271) % 2147483647;
    rain.push(state % 5 < 2 ? 0 : 1 + (Math.floor(state / 5) % 90)); // about two days in five stay dry
  }
  return rain;
}

const pair = ([total, start]) => `(${total}, ${start})`;
const sample = [12, 0, 35, 4, 0, 61, 8, 17, 0, 3];
const log = season(122, 7);
console.log(`first days of the season: [${log.slice(0, 10).join(", ")}]`);
console.log("log       k  brute force  window");
for (const [name, rain, k] of [["sample", sample, 3], ["sample", sample, 1], ["sample", sample, 10], ["season", log, 7]]) {
  // a typical k, the smallest, k = n (a single window), and a week of the made-up season
  console.log(`${name.padEnd(8)}${String(k).padStart(3)}  ${pair(wettestBrute(rain, k)).padEnd(11)}  ${pair(wettestWindow(rain, k))}`);
}

Output

first days of the season: [80, 52, 33, 86, 0, 7, 0, 0, 0, 87]
log       k  brute force  window
sample    3  (86, 5)      (86, 5)
sample    1  (61, 5)      (61, 5)
sample   10  (140, 0)     (140, 0)
season    7  (362, 82)    (362, 82)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node wettest_week.mjs

Both programs print the same lines. The brute force and the window agree on the sample for k = 3, for k = 1 and for k = n, and on a made-up season of 122 days, whose wettest week starts on day 82 with 362 mm. The season comes from a small random generator written out in each language: multiply by 48,271 and keep the remainder after dividing by 2³¹ − 1. The C++ standard defines the same generator as std::minstd_rand, so every language gets the same log from the same seed. JavaScript stores numbers as 64-bit floating point, which is exact for whole numbers below 2⁵³, and no product the generator forms comes near that.

Three details of the code are worth saying aloud. Python’s rain[start:start + k] copies k items before sum adds them, and Python’s time-complexity table lists a slice as O(j − i), so the brute force really is O(n·k) even though its inner loop is hidden. The JavaScript brute force finds the largest total with reduce, because MDN warns that spreading a long array into Math.max can fail or return a wrong result. And Python’s own documentation uses the same idea: the moving_average recipe of collections keeps a running sum s and updates it with s += elem - d.popleft(). The sliding_window recipe of itertools yields each run of n items from a deque with a maximum length instead; building each tuple copies all n items, so when you only need a total it costs as much as the brute force.

The same window in C++

wettest_week.cpp C++ · cpp/wettest_week.cpp
// The wettest k consecutive days of a rain log with a fixed-size sliding window (C++23).
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <random>
#include <utility>
#include <vector>

// Returns {total, first day} of the wettest k consecutive days. Needs 1 <= k <= rain.size(): with a larger k the
// first loop would read past the end of the vector. Check it as k > rain.size(), never as rain.size() - k < 0:
// rain.size() is unsigned, so the subtraction wraps around to a huge number and is never below zero.
std::pair<long long, std::size_t> wettest_window(const std::vector<int>& rain, std::size_t k) {
    long long total = 0;
    for (std::size_t day = 0; day < k; ++day) total += rain[day];  // the first window: days 0 to k - 1
    long long best_total = total;
    std::size_t best_start = 0;
    for (std::size_t right = k; right < rain.size(); ++right) {
        total += rain[right] - rain[right - k];  // day `right` enters, day `right - k` leaves
        if (total > best_total) {                // strictly greater, so the earliest stretch wins a tie
            best_total = total;
            best_start = right - k + 1;
        }
    }
    return {best_total, best_start};
}

// The same made-up rain log as the Python and JavaScript versions: they write out by hand the generator that the
// standard library calls std::minstd_rand (multiply by 48271, keep the remainder modulo 2^31 - 1).
std::vector<int> season(int days, std::uint_fast32_t seed) {
    std::minstd_rand next(seed);
    std::vector<int> rain;
    for (int i = 0; i < days; ++i) {
        const auto state = next();
        rain.push_back(state % 5 < 2 ? 0 : static_cast<int>(1 + state / 5 % 90));  // about two days in five stay dry
    }
    return rain;
}

int main() {
    const std::vector<int> sample{12, 0, 35, 4, 0, 61, 8, 17, 0, 3};
    const std::vector<int> season_log = season(122, 7);
    struct Case { const char* name; const std::vector<int>& rain; std::size_t k; };
    const Case cases[] = {{"sample", sample, 3}, {"sample", sample, 1}, {"sample", sample, 10}, {"season", season_log, 7}};
    std::cout << "log       k  window\n";
    for (const Case& c : cases) {
        auto [total, start] = wettest_window(c.rain, c.k);
        std::cout << std::left << std::setw(8) << c.name << std::right << std::setw(3) << c.k << "  (" << total << ", "
                  << start << ")\n";
    }
}
Built with clang++ and run Python · cpp/build_and_run.py
"""Builds wettest_week.cpp with clang++ (C++23, warnings, AddressSanitizer and UndefinedBehaviorSanitizer) and runs
it, printing each command and what it printed, as a terminal shows them."""
import subprocess

COMMANDS = [
    "clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined wettest_week.cpp -o wettest_week",
    "./wettest_week",
]

for command in COMMANDS:
    print(f"$ {command}")
    result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
    print(result.stdout, end="")
    if result.returncode != 0:
        print(f"[exit status {result.returncode}]")
        break

Output

$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined wettest_week.cpp -o wettest_week
$ ./wettest_week
log       k  window
sample    3  (86, 5)
sample    1  (61, 5)
sample   10  (140, 0)
season    7  (362, 82)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 build_and_run.py

The C++ version prints the same results. The build turns on the compiler’s warnings and two run-time checkers: AddressSanitizer, which reports any read or write outside the vector, and UndefinedBehaviorSanitizer, which reports arithmetic that the standard leaves undefined, such as a signed overflow. The build printed no warning, and the run printed no report. C++ adds one trap of its own: rain.size() is unsigned, so a guard written as rain.size() - k < 0 can never be true, because the subtraction wraps around to a huge positive number. Compare k > rain.size() instead.

A dry run

In a live round, trace the window on the sample before you trust it, one row per step:

The fixed window on the sample, step by step Python · week/trace.py
"""A dry run of the fixed window on the sample log: one row per step, with the day that enters and the day that leaves."""
rain = [12, 0, 35, 4, 0, 61, 8, 17, 0, 3]
k = 3

total = sum(rain[:k])  # the first window, days 0-2: 12 + 0 + 35
best_total, best_start = total, 0
print("days  enters  leaves  total  best")
print(f"0-2   {'':>6}  {'':>6}  {total:>5}  {best_total} (day {best_start})")
for right in range(k, len(rain)):
    total += rain[right] - rain[right - k]
    if total > best_total:
        best_total, best_start = total, right - k + 1
    days = f"{right - k + 1}-{right}"
    print(f"{days:<4}  {rain[right]:>6}  {rain[right - k]:>6}  {total:>5}  {best_total} (day {best_start})")

Output

days  enters  leaves  total  best
0-2                      47  47 (day 0)
1-3        4      12     39  47 (day 0)
2-4        0       0     39  47 (day 0)
3-5       61      35     65  65 (day 3)
4-6        8       4     69  69 (day 4)
5-7       17       0     86  86 (day 5)
6-8        0      61     25  86 (day 5)
7-9        3       8     20  86 (day 5)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace.py

Read a row aloud: “day 5 enters with 61 mm and day 2 leaves with 35, so the total rises by 26, from 39 to 65, which beats 47.” Each total is the previous one plus the day that enters minus the day that leaves, and the best changes only on rows where the new total is larger. If your own code prints a different table, the first row that differs points at the bug.

Edge cases and the usual bugs

  • k = n: one window. The loop body never runs and the first window is the answer, as the k = 10 line shows.
  • k = 1: the largest single value.
  • Ties: > keeps the earliest stretch and >= the latest; check which one the problem asks for.
  • The wrong day leaves: the day that leaves the window ending at right is right - k. Subtracting rain[right - k + 1] removes a day that is still inside.
  • Starting the best at 0: harmless for rainfall, wrong when values can be below zero, such as night temperatures. Start from the first window instead; the exercise at the end has readings below zero to catch this.
  • k larger than n: there is no window at all. The limits here rule it out; when they do not, check it first.

How much work the window saves

Additions and subtractions, counted Python · week/steps.py
"""How many daily values each version adds or subtracts, counted while its loops run."""


def brute_force_steps(n, k):
    steps = 0
    for start in range(n - k + 1):  # every stretch of k days ...
        for day in range(start, start + k):  # ... is added up from scratch
            steps += 1
    return steps


def window_steps(n, k):
    steps = 0
    for day in range(k):  # the first window is added up once
        steps += 1
    for right in range(k, n):  # then each step adds one day and subtracts one
        steps += 2
    return steps


print(f"{'n':>7}  {'k':>6}  {'brute force':>13}  {'window':>7}")
for n, k in [(10, 3), (122, 7), (122, 61), (2_000, 1_000)]:
    print(f"{n:>7,}  {k:>6,}  {brute_force_steps(n, k):>13,}  {window_steps(n, k):>7,}")

# The problem's limits, from the formulas that the counts above follow: (n - k + 1) * k and k + 2 * (n - k).
n, k = 100_000, 50_000
print("at the limits, from the formulas:")
print(f"{n:>7,}  {k:>6,}  {(n - k + 1) * k:>13,}  {k + 2 * (n - k):>7,}")

Output

      n       k    brute force   window
     10       3             24       17
    122       7            812      237
    122      61          3,782      183
  2,000   1,000      1,001,000    3,000
at the limits, from the formulas:
100,000  50,000  2,500,050,000  150,000

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 steps.py

The brute force’s count grows with n × k, the window’s only with n. On the 122-day season with k = 7 the brute force does 812 additions against the window’s 237; with k = 61 it does 3,782 against 183; at n = 2,000 and k = 1,000 it does 1,001,000 against 3,000. At the problem’s limits the formulas give about 2.5 billion against 150,000.

The variable window: grow on the right, shrink on the left

When the question is “the longest stretch that obeys a rule” or “the shortest stretch that reaches a goal”, the length is the unknown. The variable window moves right on by one item at every step and moves left on only when it has to:

The variable window as a loop: add the day that enters on the right, drop days on the left while the total is over the limit, then record.Move right on one day:add rain[right]to the totalTotal overthe limit?Subtract rain[left],move left on one dayThe window fits:its lengthright − left + 1is a candidateyescheck againnonext day

The variable window, one day at a time

Text description of the diagram

The diagram is a loop of four steps, from top to bottom.

  1. The right end of the window moves on one day, and that day's rain is added to the total.
  2. A diamond asks whether the total is now over the limit.
  3. If it is, the rain of the day at the left end is subtracted and the left end moves on one day; then the question is asked again. This can repeat several times in one step.
  4. When the total is no longer over the limit, the window fits, and its length, right − left + 1, is a candidate for the answer. Then the loop goes back to step 1 for the next day.

The left end never moves back, so over the whole log it moves at most as many times as there are days.

Here it is twice, on the same rain log: the longest spell whose total stays within a limit, and the shortest stretch that collects at least a target, say for a tank that has to fill.

Longest within a limit, shortest reaching a target

Python · spells/spells.py

"""Variable-size windows over a rain log, where no day is below zero: the longest spell that stays under a limit,
and the shortest stretch that reaches a target."""


def longest_within(rain, limit):
    """The longest stretch of consecutive days whose total is at most `limit`. Returns (length, first day)."""
    best_len, best_start = 0, 0
    total = left = 0
    for right, mm in enumerate(rain):
        total += mm  # day `right` enters
        while total > limit:  # too wet: drop days from the left until the window fits again
            total -= rain[left]
            left += 1
        if right - left + 1 > best_len:  # the window is valid here, so it is a candidate
            best_len, best_start = right - left + 1, left
    return best_len, best_start


def shortest_reaching(rain, target):
    """The fewest consecutive days whose total is at least `target` (1 or more). Returns (length, first day),
    or (0, -1) when even the whole log falls short."""
    best_len, best_start = 0, -1
    total = left = 0
    for right, mm in enumerate(rain):
        total += mm
        while total >= target:  # valid: record it, then see whether a shorter window ending here still is
            if best_len == 0 or right - left + 1 < best_len:
                best_len, best_start = right - left + 1, left
            total -= rain[left]
            left += 1
    return best_len, best_start


sample = [12, 0, 35, 4, 0, 61, 8, 17, 0, 3]
print("longest within 20 mm:", longest_within(sample, 20))
print("longest within 0 mm:", longest_within(sample, 0))
print("shortest reaching 100 mm:", shortest_reaching(sample, 100))
print("shortest reaching 200 mm:", shortest_reaching(sample, 200))

Output

longest within 20 mm: (3, 7)
longest within 0 mm: (1, 1)
shortest reaching 100 mm: (4, 2)
shortest reaching 200 mm: (0, -1)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 spells.py

JavaScript · spells/spells.mjs

// Variable-size windows over a rain log, where no day is below zero: the longest spell that stays under a limit,
// and the shortest stretch that reaches a target.

// The longest stretch of consecutive days whose total is at most `limit`. Returns [length, first day].
function longestWithin(rain, limit) {
  let bestLen = 0;
  let bestStart = 0;
  let total = 0;
  let left = 0;
  for (let right = 0; right < rain.length; right++) {
    total += rain[right]; // day `right` enters
    while (total > limit) {
      // too wet: drop days from the left until the window fits again
      total -= rain[left];
      left++;
    }
    if (right - left + 1 > bestLen) {
      // the window is valid here, so it is a candidate
      bestLen = right - left + 1;
      bestStart = left;
    }
  }
  return [bestLen, bestStart];
}

// The fewest consecutive days whose total is at least `target` (1 or more). Returns [length, first day],
// or [0, -1] when even the whole log falls short.
function shortestReaching(rain, target) {
  let bestLen = 0;
  let bestStart = -1;
  let total = 0;
  let left = 0;
  for (let right = 0; right < rain.length; right++) {
    total += rain[right];
    while (total >= target) {
      // valid: record it, then see whether a shorter window ending here still is
      if (bestLen === 0 || right - left + 1 < bestLen) {
        bestLen = right - left + 1;
        bestStart = left;
      }
      total -= rain[left];
      left++;
    }
  }
  return [bestLen, bestStart];
}

const pair = ([length, start]) => `(${length}, ${start})`;
const sample = [12, 0, 35, 4, 0, 61, 8, 17, 0, 3];
console.log("longest within 20 mm:", pair(longestWithin(sample, 20)));
console.log("longest within 0 mm:", pair(longestWithin(sample, 0)));
console.log("shortest reaching 100 mm:", pair(shortestReaching(sample, 100)));
console.log("shortest reaching 200 mm:", pair(shortestReaching(sample, 200)));

Output

longest within 20 mm: (3, 7)
longest within 0 mm: (1, 1)
shortest reaching 100 mm: (4, 2)
shortest reaching 200 mm: (0, -1)

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node spells.mjs

The two functions differ in where they record a candidate. longest_within records after its while loop, once the window fits again. shortest_reaching records inside the loop, on every window that still reaches the target, and then shrinks it to see whether a shorter one does too. On the sample, the longest stretch with at most 20 mm is days 7 to 9 (17 + 0 + 3 = 20), and the shortest with at least 100 mm is days 2 to 5 (35 + 4 + 0 + 61). A limit of 0 asks for the longest run of completely dry days: a single day, because no two dry days of the sample are neighbours. A target of 200 mm is never reached, so that answer is (0, -1).

Why dropping days is safe. No day has negative rainfall, so if days left to right hold more than the limit, every longer stretch that starts at left and ends at right or later holds at least as much. Day left can never start an answer again, and the window drops it for good.

Why a loop inside a loop is still O(n). right moves n times. left only moves forward and never passes right + 1, so over the whole run it moves at most n times, however those moves are spread over the steps. The while loop therefore runs at most n times in total, not n times per step. This dry run counts the moves:

The longest spell within 20 mm, step by step Python · spells/trace_spell.py
"""A dry run of the variable window: the longest spell of the sample log with at most 20 mm in total."""
rain = [12, 0, 35, 4, 0, 61, 8, 17, 0, 3]
limit = 20

best_len, best_start = 0, 0
total = left = 0
left_moves = 0
print("day  mm  dropped      window  total")
for right, mm in enumerate(rain):
    total += mm  # day `right` enters
    dropped = []
    while total > limit:
        dropped.append(rain[left])
        total -= rain[left]
        left += 1
        left_moves += 1
    if right - left + 1 > best_len:
        best_len, best_start = right - left + 1, left
    window = f"{left}-{right}" if left <= right else "empty"
    print(f"{right:>3}  {mm:>2}  {str(dropped) if dropped else '-':<11}  {window:<6}  {total:>5}")
print(f"longest: {best_len} days, from day {best_start}")
print(f"right moved {len(rain)} times, left {left_moves} times")
print(f"{len(rain) + left_moves} moves in all; the bound is {2 * len(rain)}")

Output

day  mm  dropped      window  total
  0  12  -            0-0        12
  1   0  -            0-1        12
  2  35  [12, 0, 35]  empty       0
  3   4  -            3-3         4
  4   0  -            3-4         4
  5  61  [4, 0, 61]   empty       0
  6   8  -            6-6         8
  7  17  [8]          7-7        17
  8   0  -            7-8        17
  9   3  -            7-9        20
longest: 3 days, from day 7
right moved 10 times, left 7 times
17 moves in all; the bound is 20

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 trace_spell.py

On day 2, 35 mm arrive and three days leave at once, day 2 itself among them, so the window is empty for a moment; on most days nothing leaves. Ten moves of right and seven of left make 17 moves for 10 days, within the bound of 20.

When the window breaks: values below zero

Now let values fall. A water tank logs its level change each hour, inflow minus use, and the question is whether some stretch of consecutive hours changes the level by exactly 2. When no change is negative, a variable window answers it: grow, shrink while the total is above the target, and stop when it equals the target. On [5, -3, 4] it gets it wrong:

A window, a brute force and prefix sums Python · negatives/net_change.py
"""A stretch of hours whose changes add up to exactly `target`: a variable window, a brute force and prefix sums.
Each returns (first hour, last hour) of a stretch it finds, or None."""


def stretch_window(changes, target):
    """Grow on the right; shrink while the total is over the target. Right only when no change is below zero."""
    total = left = 0
    for right, change in enumerate(changes):
        total += change
        while total > target and left <= right:
            total -= changes[left]
            left += 1
        if total == target and left <= right:
            return left, right
    return None


def stretch_brute(changes, target):
    """Every stretch, from every first hour."""
    for first in range(len(changes)):
        total = 0
        for last in range(first, len(changes)):
            total += changes[last]
            if total == target:
                return first, last
    return None


def stretch_prefix(changes, target):
    """Prefix sums and a dict, for changes of any sign: the hours first..last add up to prefix[last + 1] - prefix[first],
    so look for an earlier prefix total equal to the current one minus the target."""
    first_seen = {0: 0}  # prefix total -> how many hours it covers (the earliest such position)
    prefix = 0
    for last, change in enumerate(changes):
        prefix += change
        if prefix - target in first_seen:
            return first_seen[prefix - target], last
        first_seen.setdefault(prefix, last + 1)
    return None


if __name__ == "__main__":
    for changes, target in [([3, 1, 0, 2, 5], 3), ([5, -3, 4], 2)]:
        print(f"{changes}, target {target}:")
        print("  window     ", stretch_window(changes, target))
        print("  brute force", stretch_brute(changes, target))
        print("  prefix sums", stretch_prefix(changes, target))

Output

[3, 1, 0, 2, 5], target 3:
  window      (0, 0)
  brute force (0, 0)
  prefix sums (0, 0)
[5, -3, 4], target 2:
  window      None
  brute force (0, 1)
  prefix sums (0, 1)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 net_change.py

At hour 0 the total, 5, is above the target, so the window drops hour 0 for good. But hours 0 and 1 together make 5 − 3 = 2. Dropping was safe only because a stretch that was too large could never become right by growing; once a later value can be negative, growing can make the total smaller again. The rule has lost its monotone validity, and nothing tells the window when shrinking is safe.

The fix is prefix sums. Let prefix[j] be the total of the first j hours, with prefix[0] = 0. The hours first to last add up to prefix[last + 1] - prefix[first], so a stretch ending at last hits the target exactly when an earlier prefix total equals prefix[last + 1] - target. A dict of the prefix totals seen so far answers that in O(1) time on average, according to Python’s time-complexity table, so stretch_prefix makes one scan of the data and works whatever the signs. A random test shows where the boundary lies:

Each version against the brute force Python · negatives/stress.py
"""Random tests: each version against the brute force, first with no change below zero, then with negative changes.
The lists come shortest first, so the first disagreement found is a small one."""
import random

from net_change import stretch_brute, stretch_prefix, stretch_window


def random_cases(lowest):
    rng = random.Random(3)  # a fixed seed: both versions get the same 2,000 lists on every run of one Python version
    for length in range(1, 9):
        for _ in range(250):
            yield [rng.randint(lowest, 5) for _ in range(length)], rng.randint(0, 8)


for lowest in (0, -5):
    print(f"changes from {lowest} to 5:")
    for name, version in [("window", stretch_window), ("prefix sums", stretch_prefix)]:
        checked = 0
        for changes, target in random_cases(lowest):
            # Several stretches may add up to the target, so compare whether one was found at all.
            if (version(changes, target) is None) != (stretch_brute(changes, target) is None):
                print(f"  {name}: wrong on {changes}, target {target}")
                print(f"    {version(changes, target)}, brute force {stretch_brute(changes, target)}")
                break
            checked += 1
        else:
            print(f"  {name}: right on all {checked:,} lists")

Output

changes from 0 to 5:
  window: right on all 2,000 lists
  prefix sums: right on all 2,000 lists
changes from -5 to 5:
  window: wrong on [3, -1], target 2
    None, brute force (0, 1)
  prefix sums: right on all 2,000 lists

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 stress.py

With changes from 0 to 5 the window agrees with the brute force on all 2,000 lists. Once changes can go down to −5 it fails almost at once, on the two-hour list [3, -1] with target 2: 3 alone is above 2, so it is dropped before −1 arrives. The prefix-sum version agrees on both sets. The fixed seed makes every run test the same lists; Python’s documentation guarantees the same sequence across versions only for random(), and randint may change in a later version, so replay a failure on the same Python. Rules with “at most” or “at least” and values below zero also start from prefix sums, but they look for a range of earlier totals rather than one exact value, so a dict is not enough there: they need the prefix totals kept in order.

Telling the variants apart

Most window questions take one of a few shapes, and the shape tells you what the window has to keep:

The question asks for Use
The highest total or average of k consecutive items A fixed window with a running total
Every place where a rearrangement of a pattern occurs A fixed window as long as the pattern, with letter counts and a count of the letters that match
The longest stretch without a repeated item A variable window that remembers each item’s last position
The longest stretch with at most k distinct values A variable window with a count per value, deleting a value when its count reaches 0
The shortest stretch that contains every required item A variable window that counts how many of each required item are still missing
The largest value in every window of k items A fixed window with a queue of candidate positions instead of a total
How many stretches have exactly k of something Two variable windows: the count for “at most k” minus the count for “at most k − 1”
A total over a stretch when values can be negative No window: prefix sums

Interview questions

What is the difference between a fixed and a variable sliding window, and how do you tell which one a problem needs? Both keep a summary of a stretch of consecutive items between two indices that only move forward. A fixed window always holds k items: each step adds the item that enters and removes the one that leaves, so it fits questions that name the length, such as the highest total of any 7 days. A variable window grows on the right at every step and shrinks on the left while it breaks a rule; it fits questions where the length is the unknown, such as the longest stretch with at most k distinct values or the shortest stretch whose total reaches a target. The variable form is correct only when validity is monotone: shrinking a valid window keeps it valid, or, for shortest-window questions, growing it does. Rules about totals lose that property as soon as values can be negative.

A variable window has a while loop inside a for loop. Why does it still run in O(n) time? Count pointer moves instead of loop iterations. The for loop moves right forward n times. Each pass of the while loop moves left forward one place, left never moves back and never passes right + 1, so across the whole scan the while loop runs at most n times in total, however unevenly those passes are spread. Each move does O(1) work on the summary, so the whole scan is at most 2n moves: O(n). This is an amortised bound: one step can shrink the window by many items, but only items that entered earlier can leave. The argument fails if the code ever moves left backwards or rebuilds the summary from scratch inside the loop.

Python Online Compiler Paste wettest_week.py, change the log or k, and check the window against the brute force on your own data. JavaScript & TypeScript Online Compiler Run spells.mjs on a log of your own, then give one day a negative value and compare the answer with a brute force.

Key takeaways

  • A sliding window keeps a summary of consecutive items between two indices that only move forward, so each item is added once and removed at most once: O(n) instead of O(n·k) or O(n²).
  • Fixed size k: add the item that enters, subtract the item k places back, and record once the first window is full.
  • Variable size: extend right every step, shrink from left while the window breaks the rule, and record where the window is valid (after the shrinking for longest, inside it for shortest).
  • The variable window needs monotone validity. Totals lose it when values can be below zero; switch to prefix sums, with a dict for an exact target.
  • State the invariant, trace a small case, and test the window against a brute force on many small random inputs.

Exercise

Exercise · Easy · Python

The highest average over k consecutive readings

A weather station on a hill logs the lowest temperature of each night in whole degrees Celsius, so a reading can be below zero. Write best_k_day_average(readings, k) in average.py: it returns the highest average of any k consecutive readings, rounded to 5 decimal places with round(average, 5).

Limits: 1 ≤ k ≤ len(readings) ≤ 100,000, and every reading is between −40 and 40.

best_k_day_average([3, -1, 4, 1, -5, 9], 2)  # 2.5: the nights 4 and 1
best_k_day_average([3, -1, 4, 1, -5, 9], 3)  # 2.0: the nights 3, -1 and 4
best_k_day_average([-6, -2, -9, -4], 2)      # -4.0: every average is below zero
best_k_day_average([1, 0, 1], 3)             # 0.66667

Keep the running total as a whole number and divide once, at the end. The sample tests check the examples above, k equal to the number of readings, k = 1, 500 random logs against a brute force of their own (they print the first log where your answer differs), and how many readings your function looks at: on 1,000 readings with k = 500, a version that adds up every stretch from scratch looks at more than 250,000; a sliding window needs fewer than 2,000.

Starter code · average.py

def best_k_day_average(readings, k):
    """The highest average of k consecutive readings, rounded to 5 decimal places."""
    # Replace this line with your code.
    return 0.0
The sample tests · test_average.py
import random

from average import best_k_day_average


def trusted(readings, k):
    """A brute force the tests trust: every stretch of k readings, added up from scratch."""
    return round(max(sum(readings[i:i + k]) for i in range(len(readings) - k + 1)) / k, 5)


class CountingReadings(list):
    """A list that counts how many readings are looked at: by index, by slice or by iterating over it."""

    reads = 0

    def __getitem__(self, index):
        value = super().__getitem__(index)
        self.reads += len(value) if isinstance(index, slice) else 1
        return value

    def __iter__(self):
        for value in super().__iter__():
            self.reads += 1
            yield value


def test_examples():
    """the examples from the prompt"""
    assert best_k_day_average([3, -1, 4, 1, -5, 9], 2) == 2.5
    assert best_k_day_average([3, -1, 4, 1, -5, 9], 3) == 2.0


def test_all_below_zero():
    """every average is below zero, so the best one is too"""
    assert best_k_day_average([-6, -2, -9, -4], 2) == -4.0


def test_rounded():
    """the average is rounded to 5 decimal places"""
    assert best_k_day_average([1, 0, 1], 3) == 0.66667
    assert best_k_day_average([5, -40, 2, 2, 1], 3) == 1.66667


def test_whole_log():
    """k equal to the number of readings: one window"""
    assert best_k_day_average([7, -3, 10, 0], 4) == 3.5


def test_single_nights():
    """k = 1: the warmest single night"""
    assert best_k_day_average([-8, 12, 3, 12, -1], 1) == 12.0


def test_input_unchanged():
    """the caller's list is not changed"""
    readings = [4, -2, 6, 1]
    best_k_day_average(readings, 2)
    assert readings == [4, -2, 6, 1]


def test_random_logs():
    """500 random logs: the same answers as a trusted brute force"""
    rng = random.Random(41)
    for _ in range(500):
        readings = [rng.randint(-40, 40) for _ in range(rng.randint(1, 12))]
        k = rng.randint(1, len(readings))
        expected = trusted(readings, k)
        got = best_k_day_average(list(readings), k)
        assert got == expected, f"best_k_day_average({readings}, {k}) returned {got}, expected {expected}"


def test_reads_each_reading_about_twice():
    """1,000 readings with k = 500: the function looks at fewer than 3,000 readings"""
    rng = random.Random(8)
    plain = [rng.randint(-40, 40) for _ in range(1_000)]
    readings = CountingReadings(plain)
    assert best_k_day_average(readings, 500) == trusted(plain, 500)
    assert readings.reads < 3_000, (
        f"it looked at {readings.reads:,} readings; a window adds each reading once and subtracts it once, "
        "about 2,000 looks in all"
    )
A hint

Two neighbouring stretches of k nights share k - 1 of them. When the window moves one night to the right, which reading joins the total, which one leaves it, and what should the best total start as, when every reading may be below zero?

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 Which of these questions is a sliding-window problem?

    Choose one answer.

    Show the answer to question 1

    Answer: The longest run of consecutive days with at most 30 mm of rain in total

    A window needs a contiguous stretch and a rule that stays true when the stretch shrinks. "Consecutive days with at most 30 mm" has both, since rainfall is never negative. Two days with a given total are a pair (hashing), a sequence that may skip days is not contiguous, and the median of everything involves no stretch at all.

  2. Question 2 of 6 A fixed window of k days has just been extended to end at day right. Which day must it subtract?

    Choose one answer.

    Show the answer to question 2

    Answer: rain[right - k]

    The window ending at right covers days right − k + 1 to right, so the day that just fell out is right − k. Subtracting right − k + 1 removes a day that is still inside, which is the most common bug of this template.

  3. Question 3 of 6 For the 10-day sample log with k = 3, how many additions and subtractions does the window version do in all?

    Type a number.

    Show the answer to question 3

    Answer: 17

    The first window adds its 3 days, then each of the remaining 7 days costs one addition and one subtraction: 3 + 2 × 7 = 17. The brute force adds 3 days for each of the 8 windows, 24 in all, as steps.py counts.

  4. Question 4 of 6 spells.py runs the two variable windows on the sample log [12, 0, 35, 4, 0, 61, 8, 17, 0, 3]. What does it print?

    What does this program print? Choose one answer.

    """Variable-size windows over a rain log, where no day is below zero: the longest spell that stays under a limit,
    and the shortest stretch that reaches a target."""
    
    
    def longest_within(rain, limit):
        """The longest stretch of consecutive days whose total is at most `limit`. Returns (length, first day)."""
        best_len, best_start = 0, 0
        total = left = 0
        for right, mm in enumerate(rain):
            total += mm  # day `right` enters
            while total > limit:  # too wet: drop days from the left until the window fits again
                total -= rain[left]
                left += 1
            if right - left + 1 > best_len:  # the window is valid here, so it is a candidate
                best_len, best_start = right - left + 1, left
        return best_len, best_start
    
    
    def shortest_reaching(rain, target):
        """The fewest consecutive days whose total is at least `target` (1 or more). Returns (length, first day),
        or (0, -1) when even the whole log falls short."""
        best_len, best_start = 0, -1
        total = left = 0
        for right, mm in enumerate(rain):
            total += mm
            while total >= target:  # valid: record it, then see whether a shorter window ending here still is
                if best_len == 0 or right - left + 1 < best_len:
                    best_len, best_start = right - left + 1, left
                total -= rain[left]
                left += 1
        return best_len, best_start
    
    
    sample = [12, 0, 35, 4, 0, 61, 8, 17, 0, 3]
    print("longest within 20 mm:", longest_within(sample, 20))
    print("longest within 0 mm:", longest_within(sample, 0))
    print("shortest reaching 100 mm:", shortest_reaching(sample, 100))
    print("shortest reaching 200 mm:", shortest_reaching(sample, 200))
    Show the answer to question 4

    Answer: it prints

    longest within 20 mm: (3, 7)
    longest within 0 mm: (1, 1)
    shortest reaching 100 mm: (4, 2)
    shortest reaching 200 mm: (0, -1)

    Days 7 to 9 hold 17 + 0 + 3 = 20 mm, three days. No two dry days are neighbours, so the longest dry run is one day, day 1. Days 2 to 5 hold 35 + 4 + 0 + 61 = 100 mm in four days, and the whole log holds only 140 mm, so 200 mm is never reached.

  5. Question 5 of 6 A variable window drops items from the left while its rule is broken. For which of these rules is that safe?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • No letter appears twice in the window
    • The total is at most 50, and every value is 0 or more
    • The window holds at most 3 distinct values

    Dropping is safe when a window that breaks the rule cannot be repaired by growing it, so its first item can never start an answer again. That holds for a total limit with no negative values, for repeated letters and for distinct values. With negative values a total that is too high can fall again, as [5, -3, 4] with target 2 showed.

  6. Question 6 of 6 Why does the variable window run in O(n) time although it has a while loop inside a for loop?

    Choose one answer.

    Show the answer to question 6

    Answer: Because left only moves forward, so the while loop runs at most n times over the whole scan

    One step can drop several items, as day 2 of the dry run dropped three, so the per-step count is not bounded. What is bounded is the total: left moves forward only and never passes right + 1, so it moves at most n times in all, and the whole scan does at most 2n pointer moves.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.