Sliding Window Problems

Six classic interview problems — Best Time to Buy and Sell Stock, Longest Substring Without Repeating Characters, Longest Repeating Character Replacement, Permutation in String, Minimum Window Substring and Sliding Window Maximum — solved from zero with one idea: look at a stretch of neighbouring items, and when you move on, only update what changed at its two edges. For each problem you will see the slow obvious idea, why each move of the window is safe, the pseudocode, the Dart code, and an animation you can feed your own input. By the end you will know when a window applies, whether it should have a fixed or a changing size, what it must remember, and why a loop inside a loop can still be O(n).

What a sliding window is and why it is fast

You ride a train and look out of one window. At every moment you see a stretch of the fields outside. When the train moves one step, one tree disappears behind you on the left and one new tree appears on the right — everything in the middle stays in view. If someone asks "how many trees can you see?", you do not recount the whole view: you add the new tree and subtract the one that left. That is a sliding window.

Some words first:

Fixed-size windows

Some problems fix the length: "the sum of every 3 days in a row", "is any 4-letter piece of this text a rearrangement of abcd?". The window always holds exactly k items. Each step, one item enters on the right and one leaves on the left, and we update a running answer with just those two items. The brute force re-adds all k items for every window, O(n·k); the window does it in O(n). Watch the running sum:

Variable-size windows: grow right, shrink left

Most problems ask for the longest or shortest window that obeys a rule ("sum at most 6", "no repeated letter", "contains every letter of abc"). Then the length changes. Every variable window follows the same loop:

  1. Grow: move right one step and add the new item to what the window remembers.
  2. Shrink while broken: while the window breaks the rule, remove nums[left] from what the window remembers and move left one step.
  3. Record: now the window obeys the rule again — compare its length with the best so far.

The promise that makes this correct is called the invariant (a fact that is true every time the program reaches a certain line): after the inner loop, the window [left, right] is always valid, and it is the longest valid window that ends at right. Why the longest? Because left only moves when it must: every start further left was already proven broken (for this right or an earlier, shorter one — and when all numbers are 0 or more, a longer window that contains a broken one is broken too).

Why a loop inside a loop is still O(n)

The code above has a while inside a for, which usually smells like O(n²). Count the moves instead of the loops. right moves exactly n times in total. left only ever moves forward and can never pass the end, so over the whole run it moves at most n times — no matter how those moves are spread out. Some steps of right cause no shrinking, one step might cause five, but the total is capped. Every item enters once and leaves at most once: at most 2n moves, O(n). This way of counting the total instead of the worst single step is called amortized analysis — see C17 · Amortized Analysis. The counter under the animation above shows it live.

What the window remembers

The window is only fast if adding or removing one item updates its "memory" in O(1). Pick the smallest thing that answers the question:

The questionWhat the window keepsUpdate when an item enters / leavesUsed by
Sum or count of somethingOne numberadd / subtractFixed sums, Buy and Sell Stock (the cheapest day)
"Any repeat?" / "how many of each?"A counts map or a last-seen map (D27 · Set & Map)count + 1 / count − 1Longest Substring, Character Replacement, Permutation, Minimum Window
"Does it match a target mix?"Two count tables + one "how many letters match" counterfix the counter for the one letter that changedPermutation in String, Minimum Window Substring
Biggest or smallest item in the windowA deque (double-ended queue) of indexes (D28 · Queue & ListQueue)pop useless items from the back / expired ones from the frontSliding Window Maximum
Ask three questions for any "contiguous piece" problem: (1) Is the length fixed or does it change? (2) What must the window remember so that one item entering or leaving is an O(1) update? (3) If the length changes: when is the window broken, and does shrinking always repair it? If you can answer all three, a sliding window solves it in one pass.
When a window does NOT work. The shrink step relies on "removing an item moves the window toward valid". With negative numbers, removing an item can make a sum bigger, so "sum at least 8" or "sum at most 6" windows can give wrong answers (question p03-q11 shows one). Then use prefix sums with a map (Step 13.1) or a deque of prefix sums (p03-q22).

Best Time to Buy and Sell Stock

The task. prices[i] is the price of one share on day i. You may buy one share on one day and sell it on a later day — one transaction only. Return the biggest profit you can make. If no later price is higher than an earlier one, do not trade and return 0.

Example 1
Input: prices = [8, 3, 6, 2, 9, 4]
Output: 7
Why: buy on day 3 at 2, sell on day 4 at 9.
Example 2
Input: prices = [2, 4, 1, 7]
Output: 6
Why: the cheapest day (1) comes after the first rise, and 7 − 1 = 6 beats 4 − 2 = 2.
Example 3
Input: prices = [9, 7, 4, 1]
Output: 0
Why: the price only falls, so every trade loses money.
Constraints
Speed you need: every (buy, sell) pair is 5·109 checks → O(n) time, O(1) extra space.
You walk through the days with a sticky note that says "the cheapest price I have seen so far". On each new day you ask one question: "if I had bought on my sticky-note day and sold today, how much would I make?". If today is even cheaper than the note, you do not sell — you rewrite the note. You never need to look back at older days.

Brute force. Try every buy day with every later sell day and keep the biggest difference: n(n − 1)/2 pairs, O(n²) time.

Key insight. For a fixed sell day, the best buy day is simply the cheapest day before it. So we only need the minimum so far. In window language: the window is [buy, sell]; sell grows every day; buy is the left edge, and it jumps forward to sell whenever a new cheapest day appears.

Why the jump is safe. Suppose day sell is cheaper than day buy. For any future sale, buying at the new cheaper day gives a strictly bigger profit than buying at the old one, so no future answer can start at the old buy or at any day before the new one. Throwing them away loses nothing.

An edge case: the price falls every day. Each day becomes the new cheapest day, the window keeps restarting, and the answer stays 0.

Complexity. Time O(n) — one look at each day. Space O(1) — two indexes and one number.

Common mistakes. (1) Returning max(prices) − min(prices): the maximum may come before the minimum (in [9, 7, 4, 1] that gives 8, but you cannot sell before you buy). (2) Starting best at −infinity or at prices[1] − prices[0]: when prices only fall the answer must be 0 (no trade), not a negative number. (3) Comparing every day only with the day before it (prices[i] − prices[i − 1]): that finds the best single-day rise, not the best buy-then-sell over several days.

Follow-ups interviewers ask.

The idea underneath: a running minimum is the simplest thing a window can remember — the same "keep a running answer" spirit as the prefix maxima of Step 13.2 · Trapping Rain Water.

Longest Substring Without Repeating Characters

The task. Given a string s, return the length of the longest substring (a contiguous piece of s) in which no character appears twice. Every character counts, including spaces and digits, and capital and small letters are different characters.

Example 1
Input: s = "dartboard"
Output: 6
Why: "dartbo" (positions 0 to 5) and "tboard" (3 to 8) have six different letters; any 7 letters in a row repeat one.
Example 2
Input: s = "bookkeeper"
Output: 3
Why: the double letters keep cutting the text; "per" at the end is the longest clean piece.
Example 3
Input: s = ""
Output: 0
Why: an empty string has no substring with any characters.
Constraints
Speed you need: checking every substring is O(n²) substrings × O(n) each → far too slow; even O(n²) is 2.5·109 → O(n) time, O(size of the alphabet) extra space.
You are threading beads onto a string, and the rule is "no two beads of the same colour". When a new bead matches a colour already on the string, you do not start over: you slide beads off the left end up to and including the old bead of that colour, then thread the new one. A notebook that records "where did I last thread each colour?" tells you instantly how many beads to slide off.

Brute force. For every start, extend the end until a character repeats (checking with a Set): O(n²) time in the worst case, and it repeats the same work for neighbouring starts.

Key insight. Keep the window [left, right] free of repeats. When s[right] is already inside the window at position p, no window that still contains position p can include right, so left may jump straight to p + 1 instead of creeping one step at a time. A map lastSeen (character → the last index where it appeared, built on Dart's Map from D27 · Set & Map methods) finds p in O(1).

Why the jump is safe — and the one trap. Every start between the old left and p still contains both copies, so none of them can be part of a clean window ending at right. The trap: lastSeen is never cleaned, so it may remember a position that is already left of the window. That old copy is not in the window and must be ignored — hence the check lastSeen[c] ≥ left. Without it, left could jump backwards.

An edge case: every character is the same. Each new character finds its twin at the very previous position, so the window keeps jumping and never grows past length 1.

Complexity. Time O(n) — right visits each character once and left only jumps forward. Space O(min(n, alphabet)) for the map (at most 95 printable ASCII characters).

Common mistakes. (1) Forgetting lastSeen[c] ≥ left: on "dartboard" the final d would send left back to 1 and the answer would be wrong. (2) Writing left = lastSeen[c] instead of + 1: the window would still contain the old copy. (3) Updating best before moving left: you would count a window that has a repeat. (4) Using a Set plus a creeping while is also correct and O(n) — fine as a first answer — but the jump version touches fewer items.

Follow-ups interviewers ask. At most k different characters (p03-q13), at most two kinds — the "fruit baskets" story (p03-q14), the shortest piece containing every kind of character (p03-q24). Return the substring itself (remember left when best improves). Full Unicode text: walk over runes instead of code units (D24 · String methods).

The ideas underneath: hash maps (C11 · Hash Tables) and the "have I seen this before?" pattern of Step 13.1 · Contains Duplicate.

Longest Repeating Character Replacement

The task. You get a string s of capital letters A–Z and a whole number k. You may change at most k letters into any other capital letters. Return the length of the longest substring that can be made of one single repeated letter.

Example 1
Input: s = "ABBAB", k = 1
Output: 4
Why: change the A in "BBAB" to B → "BBBB".
Example 2
Input: s = "BAABCBBA", k = 2
Output: 5
Why: positions 3 to 7 spell "BCBBA": three B's and two other letters. Change those two → "BBBBB". Every 6-letter piece has at most three copies of one letter, so it would need 3 changes.
Example 3
Input: s = "ABCD", k = 0
Output: 1
Why: no changes allowed and no letter repeats, so the best is a single letter.
Constraints
Speed you need: every substring is 5·109 → O(n) time with 26 counters, O(1) extra space.
A row of coloured tiles, and you own k cans of paint. A stretch of tiles can become one colour if the tiles that are not its most common colour number at most k — you paint exactly those. So for a window you only need two facts: its length, and how many times its most common letter appears. "Length minus most common" is the number of cans you would spend.

Brute force. For every substring, count its letters and check length − top count ≤ k: O(n²) substrings even with running counts.

Key insight. A window is fixable when (right − left + 1) − maxFreq ≤ k, where maxFreq is the count of its most common letter. Grow right; when the window is not fixable, move left one step. Because we only care about the longest answer, the window never needs to get shorter: once we have found a fixable window of length L, a shorter window cannot beat it. So when the window breaks, we slide it (left and right both move by one) instead of shrinking it. The window's length is then always the best length found so far.

Why maxFreq never needs to go down. When a letter leaves, the true top count inside the window may drop, but we keep the old maxFreq. That looks wrong, but think about when the answer can improve: only when some letter appears more than maxFreq times inside a window — and then maxFreq grows honestly at that moment. A too-high maxFreq only lets the window slide (keeping the old best length) when strictly it is not fixable; it never makes best bigger than a real answer. So recounting the 26 letters after every slide would be wasted work.

An edge case: k = 0, so no paint at all. The answer is the longest run of one letter, and the window slides whenever a different letter enters.

Complexity. Time O(n) — each step does O(1) work (no recount of 26 letters). Space O(1) — at most 26 counters.

Common mistakes. (1) Recomputing maxFreq by scanning all 26 counts after every step: still correct, but 26 times more work, and interviewers ask why it is not needed. (2) Comparing with ≥ k instead of > k: a window that needs exactly k changes is allowed. (3) Using the "shrink while broken" loop together with a never-decreasing maxFreq and then reporting the window as a real fixable substring — with the stale count the current window may not be fixable; only its length is guaranteed to be the best answer. (4) Forgetting that k can be as big as the string: then the whole string is the answer.

Follow-ups interviewers ask. Binary version: the longest run of 1s if you may flip at most k zeros (p03-q8) — the same window with "number of zeros" in place of "length − maxFreq". Delete exactly one item instead (p03-q9). Return which letter and where (store left and the top letter when best improves).

The ideas underneath: counting with a map, as in Step 13.1 · Valid Anagram and Top K Frequent.

Permutation in String

The task. You get two strings of small letters a–z: a short pattern and a longer text. A permutation of the pattern is any rearrangement of its letters ("ten", "net", "nte", …). Return true if some contiguous piece of text is a permutation of pattern, otherwise false.

Example 1
Input: pattern = "ten", text = "latent"
Output: true
Why: positions 2 to 4 of "latent" spell "ten" itself; a rearrangement such as "ent" would count too.
Example 2
Input: pattern = "dog", text = "goodbye"
Output: false
Why: the 3-letter pieces are goo, ood, odb, dby, bye — none has exactly one d, one o and one g.
Example 3
Input: pattern = "abc", text = "ab"
Output: false
Why: the text is shorter than the pattern, so no piece is long enough.
Constraints
Speed you need: sorting every piece is O(n·m log m) ≈ 109 → O(n) time with 26 counts and a match counter, O(1) extra space.
Two bags of Scrabble tiles: the pattern's bag and the window's bag. They hold the same letters exactly when, for each of the 26 letters, both bags have the same number of tiles. Instead of comparing all 26 letters every time, keep a scoreboard: "how many of the 26 letters currently agree?". When one tile goes in and one comes out, only those two letters can change their agreement, so the scoreboard needs at most two updates per step. When it reads 26, the bags match.

Brute force. For every piece of length m, sort it and compare with the sorted pattern: O(n·m log m). Or count its letters from scratch: O(n·m).

Key insight. Every candidate has the same length m, so this is a fixed-size window. Keep need (letter counts of the pattern), have (letter counts of the window) and matches (how many of the 26 letters have need = have). When a letter enters, only its own count changes, so only its agreement can flip: if have just reached need, one more letter agrees; if have just went one past need, a letter that agreed stops agreeing. Leaving works the same way in reverse.

Why it is correct. matches = 26 means every letter count agrees, which is exactly "the window is a rearrangement of the pattern". Each update changes one count by 1, and the two if checks are the only two ways that change can flip agreement, so matches is always the true count.

An edge case with no valid window: the window slides all the way to the end, matches never reaches 26, and the answer is false.

Complexity. Time O(n + 26) = O(n) — one initial count, one 26-letter scan, then O(1) per slide. Space O(1) — two lists of 26.

Common mistakes. (1) Comparing the two 26-count lists with == in Dart: two different List objects are never == even with equal contents — compare element by element (or keep the counter). (2) Checking matches only inside the loop and forgetting the very last window (the final return matches = 26). (3) Forgetting that the pattern may be longer than the text. (4) Mixing up the order of enter and leave updates is harmless — but updating matches before changing have gives wrong flips.

Follow-ups interviewers ask. Return every start position of a rearrangement (p03-q10 — the same window, collecting starts instead of stopping). Allow any characters, not just a–z (use a Map instead of 26 boxes). What if the pattern is huge and the text arrives as a stream? The window only needs the last m characters, so a queue of size m is enough.

The idea underneath: letter counts as a fingerprint, from Step 13.1 · Valid Anagram and Group Anagrams.

Minimum Window Substring

The task. You get two strings s and t. Return the shortest contiguous piece of s that contains every character of t, including repeats (if t has two A's, the piece needs at least two A's). Order does not matter, and extra characters are allowed. If no piece works, return the empty string "". If several shortest pieces exist, return the one that starts first. Capital and small letters are different.

Example 1
Input: s = "CAPABLEBAKER", t = "ABK"
Output: "BAK"
Why: positions 7 to 9 hold B, A, K — three letters, and no shorter piece can hold three different letters.
Example 2
Input: s = "TRAINSTATION", t = "TAN"
Output: "NSTA"
Why: "TRAIN" and "AINST" also work but have 5 letters; "NSTA" (positions 4 to 7) has 4.
Example 3
Input: s = "a", t = "aa"
Output: ""
Why: t needs two a's but s has only one.
Constraints
Speed you need: every piece checked is O(n²) pieces → O(|s| + |t|) time, O(number of different characters) extra space.
A shopping list says "2 eggs, 1 milk, 1 bread", and the shop's shelf is one long aisle. You walk forward putting things in your basket (grow). The moment your basket covers the whole list, you start handing back items from the oldest end of the basket (shrink), as long as the basket still covers the list. Each time it covers the list you note how long the stretch of aisle is. When handing back would break the list, you walk forward again.

Brute force. Check every piece s[i..j] with fresh counts: O(n²) pieces, each O(n) or O(alphabet) to check.

Key insight. This is the shortest-window version of the template: grow until the window is valid, then shrink while it stays valid, recording each valid length. Keep need (counts from t), have (counts in the window) and formed = how many different characters of t already have have[c] ≥ need[c]. The window is valid exactly when formed equals the number of different characters in t. As in Permutation in String, one entering or leaving character can change formed by at most one, so the validity test is O(1).

Why shrinking is safe. For a fixed right, once [left, right] is valid, any window starting further left is longer, so it cannot be the answer for this right. And once shrinking breaks validity, any start further right is also invalid for this right (it holds even fewer characters). So for each right the loop checks exactly the one window that matters: the shortest valid one ending there.

An edge case with no valid window: t needs two a's and s has one, so formed never reaches the goal and the answer is "".

Complexity. Time O(|s| + |t|) — right and left each pass over s once, and t is counted once. Space O(k) for the two maps, k = number of different characters.

Common mistakes. (1) Counting formed per copy instead of per different character, then comparing with t.length while also adding extra copies — the extra copies inflate it. Count a character as formed only at the moment have reaches need exactly. (2) Decreasing formed whenever a needed character leaves, even if the window still has more than enough copies — only when have drops below need. (3) Building the answer substring every time it improves (O(n) each) — store the start and length, cut once at the end. (4) Forgetting the "no window" case: if bestLen never changed, cutting s with it would fail or return a wrong piece — return "" first.

Follow-ups interviewers ask. Order matters (the piece must contain t as a subsequence): a forward-then-backward scan, p03-q31. The shortest piece containing every kind of character of s itself (p03-q24). Count how many valid windows exist (for each right, every start up to the first invalid one counts — the "at most" counting trick of p03-q16).

The ideas underneath: counting maps from Step 13.1, and the same-direction pointers of Step 13.2.

Sliding Window Maximum

The task. You get a list of whole numbers nums and a window length k. A window of length k slides from the left end to the right end, one step at a time. Return the biggest number inside each window, in order. If k is bigger than the list, there is no full window and the answer is empty.

Example 1
Input: nums = [4, 2, 12, 3, 8, 1, 7], k = 3
Output: [12, 12, 12, 8, 8]
Why: 12 rules the first three windows; after it slides out, 8 rules the last two.
Example 2
Input: nums = [5, 4, 3, 2, 1], k = 2
Output: [5, 4, 3, 2]
Why: in a falling list each window's maximum is its first number.
Example 3
Input: nums = [6], k = 1
Output: [6]
Why: one window holding one number.
Constraints
Speed you need: scanning each window is O(n·k) = 1010 in the worst case → O(n) time with a deque, O(k) extra space.
A queue of job candidates where each one stays for exactly k days. When a new candidate arrives, every candidate already waiting who is weaker or equal and older goes home at once — they leave earlier than the newcomer and are never better, so they can never be the best on any future day. What remains is a line from strongest (front) to weakest (back). The best candidate today is always the one at the front, unless their k days are up, in which case they leave from the front.

Brute force. For each of the n − k + 1 windows, scan its k numbers for the biggest: O(n·k). A max-heap of the window gives O(n log k) — better, but deletion of the leaving number is awkward.

Key insight. A monotonic deque: a double-ended queue (you can add or remove at both ends in O(1); Dart's ListQueue from D28 · Queue, ListQueue & LinkedList) holding indexes whose values decrease from front to back. When nums[right] arrives, pop from the back every index whose value is ≤ it — those can never be a maximum again, because right is newer (stays longer) and at least as big. Then push right. If the front index has slid out of the window (≤ right − k), pop it from the front. The front is now the window's maximum.

Why we store indexes, not values. To know whether the front has left the window we need its position. With equal values stored as values, we could not tell which copy expired.

An edge case: all numbers equal. Because the pop rule uses ≤, each new 3 removes the older 3, so the deque holds just one index and nothing ever needs to expire from the front.

Complexity. Time O(n) — every index is pushed once and popped at most once (from the back or the front), the same "enters once, leaves once" count as the opening section; the inner while cannot pop more items than were pushed. Space O(k) for the deque, plus the output.

Common mistakes. (1) Storing values instead of indexes — you cannot tell when the front expired. (2) Popping with < instead of ≤ is still correct but keeps useless equal values (more work). (3) Recording a maximum before the first window is full (right ≥ k − 1). (4) Using a plain List with removeAt(0) for the front: that shifts every element, O(k) per pop — use ListQueue.

Follow-ups interviewers ask. Window minimum (flip the comparison). Longest window whose max − min ≤ limit: two deques at once (p03-q23), and counting such windows (p03-q30). Window median: a deque cannot do it — keep the window sorted or use two heaps (p03-q26; heaps are in C06 · Heapsort). A dynamic-programming recurrence that needs "the best of the last k values" uses this same deque (p03-q28).

The ideas underneath: queues and deques (D28, C10 · Elementary Data Structures) and amortized counting (C17 · Amortized Analysis).

Quiz

Interview questions

Variations and follow-ups of the six problems above — the questions interviewers move to once you have solved the classic version, plus two stock problems that look like windows but need dynamic programming. Every solution is tested in Dart on its examples and on hundreds of random inputs against a slow reference.

Cheat sheet

ProblemWindowWhat it keepsTimeSpaceThe move and why it is safe
Buy and Sell StockVariable [buy, sell]cheapest day so farO(n)O(1)A cheaper day beats the old buy day for every future sale → buy jumps
Longest Substring Without RepeatsVariable, longestlast-seen mapO(n)O(alphabet)Repeat inside the window at p → left jumps to p + 1 (ignore p < left)
Character ReplacementVariable, never shrinks26 counts + maxFreqO(n)O(1)length − maxFreq > k → slide; a stale maxFreq never inflates best
Permutation in StringFixed, length mneed, have, matchesO(n)O(1)One letter in, one out → fix matches for those two letters only
Minimum Window SubstringVariable, shortestneed, have, formedO(|s| + |t|)O(k)Valid → record and shrink; stop when a needed count drops below need
Sliding Window MaximumFixed, length kdeque of indexes, values decreasingO(n)O(k)Pop smaller-or-equal from the back, expired from the front; front = max
TemplateShapeUse when
Fixed size kadd nums[right]; if right ≥ k remove nums[right − k]; if right ≥ k − 1 record"every k in a row", rearrangements of a fixed pattern
Longest validgrow; while broken shrink; record after the loop"longest piece with at most …"
Shortest validgrow; while valid record then shrink"shortest piece with at least / containing …"
Count windowslongest-valid loop, add right − left + 1 each step; exactly k = atMost(k) − atMost(k − 1)"how many pieces have at most / exactly …"

A window needs a contiguous piece and a rule where shrinking moves toward valid (true for counts and for sums of non-negative numbers). With negative numbers use prefix sums with a map (Step 13.1 · Arrays & Hashing); for pairs in a sorted list use opposite-end pointers (Step 13.2 · Two Pointers).