Data Structures & Algorithms (DSA) Module 2 – Arrays, strings and in-place techniques
Sliding windows: fixed and variable size
Keep a running summary of a stretch as one item enters and one leaves: fixed windows, variable windows with their invariant, and the inputs that break them.
What you will learn
- Maintain a fixed-size window sum in O(1) per step
- Apply a variable window that grows and shrinks while keeping an invariant
- Recognise when sliding windows fail and what to use instead
Before you start
What you’ll learn · Before you start
What you will learn
- Maintain a fixed-size window sum in O(1) per step
- Apply a variable window that grows and shrinks while keeping an invariant
- Recognise when sliding windows fail and what to use instead
Before you start
On this page
A sliding window is a stretch of consecutive items, from index left to index right, that moves along an array
while a summary of what it holds, such as its total, is kept up to date. Instead of adding up every stretch from
scratch, the window adds the item that enters on the right and removes the item that leaves on the left, so each
step costs O(1). A fixed window always holds k items and answers questions such as the highest seven-day average. A
variable window takes in items on the right and drops them from the left while a rule is broken, and answers
questions such as the longest stretch within a budget. Both turn scans that cost O(n·k) or O(n²) into O(n). This
lesson builds both in Python, traces them on small inputs and names the inputs on which a window gives wrong
answers.
Fixed windows: one item in, one item out
A fixed window of k readings ends at right and holds a[right − k + 1] to a[right]. Build the first total with
k additions; after that, each step adds the reading that enters and subtracts the one that leaves,
total += a[right] − a[right − k]. Over n readings that is k + 2(n − k) additions, against (n − k + 1) × k for
adding every window from scratch, so the cost of a step no longer depends on k. The program finds the highest
seven-day average in 60 days of synthetic air-quality readings both ways:
"""The highest 7-day average of 60 days of air-quality readings (synthetic), by brute force and with a fixed window."""
import random
rng = random.Random(42)
aqi = [max(20, round(110 + 60 * rng.uniform(-1, 1) + (40 if 30 <= d < 40 else 0))) for d in range(60)]
K = 7
print("first 14 days:", aqi[:14])
def brute(a, k):
"""Adds up every window of k days from scratch."""
best, adds = None, 0
for start in range(len(a) - k + 1):
total = 0
for d in range(start, start + k):
total += a[d]
adds += 1
if best is None or total > best[0]:
best = (total, start)
return best, adds
def window(a, k, trace=0):
"""Keeps one total: each step adds the day that enters and subtracts the day that leaves."""
total = sum(a[:k])
adds = k
best = (total, 0)
for right in range(k, len(a)):
total += a[right] - a[right - k] # day `right` enters, day `right - k` leaves
adds += 2
if total > best[0]:
best = (total, right - k + 1)
if right < k + trace:
print(f" day {right:>2} enters (+{a[right]}), day {right - k:>2} leaves (-{a[right - k]}): total {total}")
return best, adds
(b_total, b_start), b_adds = brute(aqi, K)
print("the first steps of the window:")
(w_total, w_start), w_adds = window(aqi, K, trace=3)
print(f"brute force: best total {b_total} from day {b_start}, {b_adds} additions")
print(f"window: best total {w_total} from day {w_start}, {w_adds} additions")
print(f"highest 7-day average: {w_total / K:.1f}, days {w_start} to {w_start + K - 1}") Output
first 14 days: [127, 53, 83, 77, 138, 131, 157, 60, 101, 54, 76, 111, 53, 74] the first steps of the window: day 7 enters (+60), day 0 leaves (-127): total 699 day 8 enters (+101), day 1 leaves (-53): total 747 day 9 enters (+54), day 2 leaves (-83): total 718 brute force: best total 1207 from day 30, 378 additions window: best total 1207 from day 30, 113 additions highest 7-day average: 172.4, days 30 to 36
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 aqi_week.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Both find days 30 to 36, with an average of 172.4, but the window needed 113 additions where the brute force needed 378. The gap grows with k: for a 30-day window over a year of daily readings, the brute force makes 10,080 additions and the window 700.
One step of a fixed window and the state of a variable window
Text description of the diagram
The top half shows the values 5, 2, 7, 1, 4, 6, 3 and 8 at positions 0 to 7. A dashed bracket over positions 1 to 3 marks the window before the step, with the total 2 + 7 + 1 = 10. Positions 2 to 4 are shaded and a solid bracket under them marks the window after the step: 2 leaves on the left, 4 enters on the right, and the new total is 10 − 2 + 4 = 12.
The bottom half shows a delivery log with the delays 4, 1, 0, 6, 2, 3, 0, 1, 9 and 2 and a budget of 8. Arrows mark left at position 2 and right at position 4, and the shaded window holds 0 + 6 + 2 = 8. The first two delays are greyed out because they have left the window: when the delay at position 4 arrived, the total reached 9, so left moved past the 1. A note says that right moves every step while left moves only while the total is over the budget.
Variable windows: grow on the right, shrink on the left
A courier firm logs the delay of every delivery in minutes, never negative, and wants the longest run of consecutive deliveries whose delays add up to at most a budget B. A variable window answers it in one pass:
- Move
righton by one and add its delay tototal. - While
total > B, subtractdelays[left]and movelefton. - The run from
lefttorightis now the longest run within budget that ends atright; compare its length with the best so far.
Step 3 relies on a monotone rule. Delays are never negative, so a run within budget stays within budget when it
loses deliveries at either end, and the deliveries left of left can be dropped for good. Here is the trace with a
budget of 8:
"""The longest run of consecutive deliveries whose delays add up to at most a budget, with a variable window."""
import random
def longest_within(delays, budget, trace=False):
"""(length, first) of the longest run with total delay <= budget; delays are never negative."""
left, total = 0, 0
best = (0, 0)
for right, d in enumerate(delays):
total += d # the delivery at `right` enters
while total > budget: # over budget: drop deliveries from the left until the run fits again
total -= delays[left]
left += 1
if right - left + 1 > best[0]:
best = (right - left + 1, left)
if trace:
print(f" right {right}: +{d:<2} window {left}..{right} total {total:>2} best {best}")
return best
def brute(delays, budget):
best = (0, 0)
for i in range(len(delays)):
total = 0
for j in range(i, len(delays)):
total += delays[j]
if total <= budget and j - i + 1 > best[0]:
best = (j - i + 1, i)
return best
log = [4, 1, 0, 6, 2, 3, 0, 1, 9, 2]
print(f"delays {log}, budget 8:")
print("result:", longest_within(log, 8, trace=True))
rng = random.Random(1)
cases = [([rng.randint(0, 9) for _ in range(rng.randint(0, 20))], rng.randint(0, 20)) for _ in range(2000)]
print("matches the brute force on 2,000 random logs:", all(longest_within(d, b) == brute(d, b) for d, b in cases)) Output
delays [4, 1, 0, 6, 2, 3, 0, 1, 9, 2], budget 8: right 0: +4 window 0..0 total 4 best (1, 0) right 1: +1 window 0..1 total 5 best (2, 0) right 2: +0 window 0..2 total 5 best (3, 0) right 3: +6 window 1..3 total 7 best (3, 0) right 4: +2 window 2..4 total 8 best (3, 0) right 5: +3 window 4..5 total 5 best (3, 0) right 6: +0 window 4..6 total 5 best (3, 0) right 7: +1 window 4..7 total 6 best (4, 4) right 8: +9 window 9..8 total 0 best (4, 4) right 9: +2 window 9..9 total 2 best (4, 4) result: (4, 4) matches the brute force on 2,000 random logs: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 delays.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
At right = 3 the total reaches 11, so the 4 leaves. At right = 8 a single delay of 9 is over the budget on its own,
so the window empties until the next delivery. The answer is a run of 4 deliveries starting at position 4. The
while loop inside the for loop does not make the scan quadratic: right moves n times, and left moves forward
at most n times in total and never back, so the whole scan is at most 2n moves, O(n).
When a window is the wrong tool
Shrinking is safe only when a shorter window keeps a valid window valid. These inputs need something else:
- Negative values. A total can rise again after it falls, so dropping items on the left can lose the answer.
- Exact totals, any signs. Prefix sums with a hash map count the stretches whose total equals a target in one pass.
- The shortest stretch reaching a target, any signs. Prefix sums with a monotonic deque find it in O(n).
- Distinct values. A window over letters or labels keeps a dictionary of counts and deletes a key at count 0.
| Fixed window | Variable window | |
|---|---|---|
| Fixed part | The number of items, k | The rule every window must keep |
| Each step | One item in, one item out | One item in, then items out until the rule holds |
| Use | The best total or average of k items | The longest or shortest stretch that keeps a rule |
| Below zero | Still correct | Wrong for rules about totals |
| Moves | 2n − k in all | At most 2n in all |
Timed drills on many window problems are in the sliding window patterns of Coding Interview Patterns.
Exercise: the shortest run that reaches a target
Exercise · Medium · Python, JavaScript
The shortest run of shops that reaches a footfall target
A market street has n shops in a row, and footfall[i] is the number of visitors shop i had yesterday, never negative. A sponsor wants the shortest run of consecutive shops whose visitors add up to at least target.
Write shortest_run(footfall, target) (JavaScript: shortestRun(footfall, target)) that returns the length of that run, or 0 if no run reaches the target. target is at least 1.
- For
[120, 40, 300, 80, 0, 90, 260]and a target of 350, the answer is 2: shops 5 and 6 have 90 + 260 visitors. - For
[50, 50]and a target of 101, the answer is 0.
Use a variable window: grow it on the right, and while it reaches the target, record its length and shrink it from the left. The sample tests check small streets, compare your function with a brute force on 300 random streets, and end with 200,000 shops, which a quadratic solution cannot finish in time.
Python · Starter code · footfall.py
def shortest_run(footfall, target):
"""Length of the shortest run of consecutive shops with total footfall >= target, or 0 if there is none."""
# Replace this line with your code.
return 0 The sample tests · test_footfall.py
import random
from footfall import shortest_run
def brute(footfall, target):
best = 0
for i in range(len(footfall)):
total = 0
for j in range(i, len(footfall)):
total += footfall[j]
if total >= target:
if best == 0 or j - i + 1 < best:
best = j - i + 1
break
return best
def test_examples():
"""the two streets of the prompt"""
assert shortest_run([120, 40, 300, 80, 0, 90, 260], 350) == 2
assert shortest_run([50, 50], 101) == 0
def test_small_streets():
"""an empty street, one shop that is enough, one that is not, and a target met only by the whole street"""
assert shortest_run([], 5) == 0
assert shortest_run([500], 500) == 1
assert shortest_run([499], 500) == 0
assert shortest_run([1, 2, 3, 4], 10) == 4
assert shortest_run([0, 0, 7, 0, 0], 7) == 1
def test_random_streets():
"""300 random streets with zeros, against the brute force"""
rng = random.Random(21)
for _ in range(300):
footfall = [rng.choice([0, 0, 1, 2, 5, 9]) for _ in range(rng.randint(0, 30))]
target = rng.randint(1, 40)
assert shortest_run(footfall, target) == brute(footfall, target), (footfall, target)
def test_long_street():
"""200,000 shops, which only a linear solution handles in time"""
footfall = [k % 10 for k in range(200_000)]
assert shortest_run(footfall, 45) == 9
assert shortest_run(footfall, 10 ** 9) == 0 JavaScript · Starter code · footfall.mjs
/** Length of the shortest run of consecutive shops with total footfall >= target, or 0 if there is none. */
export function shortestRun(footfall, target) {
// Replace this line with your code.
return 0;
} The sample tests · footfall.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { shortestRun } from './footfall.mjs';
function brute(footfall, target) {
let best = 0;
for (let i = 0; i < footfall.length; i++) {
let total = 0;
for (let j = i; j < footfall.length; j++) {
total += footfall[j];
if (total >= target) {
if (best === 0 || j - i + 1 < best) best = j - i + 1;
break;
}
}
}
return best;
}
test('the two streets of the prompt', () => {
assert.equal(shortestRun([120, 40, 300, 80, 0, 90, 260], 350), 2);
assert.equal(shortestRun([50, 50], 101), 0);
});
test('an empty street, one shop that is enough, one that is not, and a target met only by the whole street', () => {
assert.equal(shortestRun([], 5), 0);
assert.equal(shortestRun([500], 500), 1);
assert.equal(shortestRun([499], 500), 0);
assert.equal(shortestRun([1, 2, 3, 4], 10), 4);
assert.equal(shortestRun([0, 0, 7, 0, 0], 7), 1);
});
test('300 random streets with zeros, against the brute force', () => {
let seed = 21;
const random = () => (seed = (seed * 48271) % 2147483647) / 2147483647;
const choices = [0, 0, 1, 2, 5, 9];
for (let k = 0; k < 300; k++) {
const footfall = Array.from({ length: Math.floor(random() * 31) }, () => choices[Math.floor(random() * choices.length)]);
const target = 1 + Math.floor(random() * 40);
assert.equal(shortestRun(footfall, target), brute(footfall, target), JSON.stringify([footfall, target]));
}
});
test('200,000 shops, which only a linear solution handles in time', () => {
const footfall = Array.from({ length: 200000 }, (_, k) => k % 10);
assert.equal(shortestRun(footfall, 45), 9);
assert.equal(shortestRun(footfall, 10 ** 9), 0);
}); A hint
Keep left and a running total. For each right, add footfall[right]; then, while total >= target, the window from left to right reaches the target, so update the answer with its length and remove footfall[left] before moving left on. Every shop enters and leaves once.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide, QuickJS): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: Python (about 13.5 MB) or JavaScript (about 0.6 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
4 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.
Why the variable window is right and O(n), in five languages Pro: opens with a Pro, Premium or Ultimate pass
The variable window keeps one invariant, and two short arguments show that it never misses the answer and never makes more than 2n moves. This part gives both, counts the moves on four logs and runs the window in five languages.
In this part:
- The invariant, and why left never moves back
- The moves of both indices, counted
- The window in Python, JavaScript, Java, C++ and Go
3 runnable examples (C++, Go, Java, JavaScript, Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Windows over letters and labels: counts, a slip and the cost of hashing Pro: opens with a Pro, Premium or Ultimate pass
When the rule is about which values a window holds, such as deliveries to at most k different zones, the summary becomes a table of counts. This part builds it with a dictionary and with 26 counts, reproduces a slip that shrinks windows too far, and times both tables on a million deliveries.
In this part:
- A dictionary of counts, deleted at zero
- The slip that keeps zero counts, found by every short route
- 26 counts against a dictionary, measured
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
When windows fail: negative values reproduced, and two prefix-sum fixes Pro: opens with a Pro, Premium or Ultimate pass
A negative value lets a total rise again after it falls, and the variable window then gives wrong answers. This part finds the smallest such input automatically, then replaces the window with prefix sums and a hash map for exact totals, and with a monotonic deque for the shortest stretch that reaches a target.
In this part:
- The smallest counterexample, found by brute force
- Exact totals with prefix sums and a hash map
- The shortest stretch reaching a target, with a monotonic deque
- Choosing the tool
1 runnable example (Python) · about 3 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Interview questions
-
Fresher to mid · Warm-up
Why does a fixed window of k items over n items take O(n) time in total, whatever k is, when adding up every window from scratch takes O(n·k)?
Neighbouring windows share k − 1 items. Moving the window one place drops the item that leaves on the left and adds the item that enters on the right, so the total changes with two operations, not k. Building the first window costs k additions and each of the other n − k windows costs 2, which is k + 2(n − k) operations, at most 2n, so O(n) for any k. Adding every window from scratch repeats the shared items: (n − k + 1) × k additions, worst near k = n / 2 at about n² / 4. The update works for any summary that changes by one item at a time, such as a total, a count or a table of counts, but not for a maximum: when the largest item leaves, the old summary cannot tell what the new maximum is.
-
Mid to senior
A sensor reports one reading a second, and a calibration offset makes some readings negative. Find the longest run of consecutive seconds whose average is at most a threshold T, for up to 10⁶ readings. Why does a plain variable window fail here, and what do you use instead?
-
Fresher to mid
Find the shortest run of consecutive shops whose footfall reaches a target, where every footfall is 0 or more. Prove that the variable window never skips the answer, and say exactly what breaks if a shop may report a negative correction.
-
Mid to senior
Your function for the longest run with at most k distinct labels passes every test but times out on a log of 10⁶ labels. List the likely causes, how you would find the real one, and the fix for each.
Key takeaways
- A sliding window keeps a summary of a stretch up to date as items enter on the right and leave on the left.
- A fixed window of k items adds one item and subtracts one per step, k + 2(n − k) additions in all.
- A variable window grows on the right and shrinks from the left while a rule is broken;
leftnever moves back, so the scan is O(n). - The variable window needs a monotone rule: with negative values, rules about totals call for prefix sums instead.
References
- collections: deque objects (Python Software Foundation)
- Mapping types: dict (Python Software Foundation)
- time.process_time() (Python Software Foundation)
- The Java Language Specification, Java SE 25: 15.18.2 Additive operators (+ and -) for numeric types (Oracle)
- std::size_t (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: integer overflow (The Go Authors)
- Java SE 25 API: HashMap.remove (Oracle)
- Map.prototype.delete() (MDN Web Docs)
- std::unordered_map::erase (cppreference.com) (cppreference.com)
- The Go Programming Language Specification: deletion of map elements (The Go Authors)
- ACM/IEEE-CS/AAAI Computer Science Curricula 2023: Algorithmic Foundations (AL) (ACM, IEEE Computer Society and AAAI)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress