Sliding window with a frequency map
Keep a map of how many times each value appears inside a sliding window, updating it as items enter and leave, to answer questions about the window's contents.
O(n)Space O(k)11 practice problemsStep-through pictureThe idea
Some window questions are about what the window contains, not its total: no letter repeats, at most two kinds of items, every letter of a word appears. To answer them quickly, keep a frequency map: a Map from each value to how many times it appears inside the window right now.
When an item enters on the right, add one to its count. When an item leaves on the left, take one away, and delete the key when its count reaches zero. Then counts.size is the number of different values in the window, and counts.get(x) tells you how many copies of x it holds.
Combined with a growing and shrinking window, this answers questions like "the longest part of a string with at most k different characters" in one pass, without recounting the window each time.
An everyday example
A tally sheet at a museum door
A guard keeps a tally of how many visitors from each school are inside one small room. When a visitor walks in, the guard adds a mark next to their school. When one walks out, the guard rubs out a mark, and crosses the school off the sheet when its last visitor leaves. At any moment, the guard knows how many different schools are inside just by counting the names on the sheet.
| In the example | In the code |
|---|---|
| The room | The window s[left..right] |
| The tally sheet | counts, a Map |
| A visitor walks in | counts.set(ch, (counts.get(ch) ?? 0) + 1) |
| A visitor walks out | counts.set(out, counts.get(out) - 1) |
| Crossing a school off the sheet | counts.delete(out) |
| How many schools are inside | counts.size |
From the slow way to sliding window with a frequency map
- Start with the obvious way: for each start
left, extend the end one character at a time, put each character into a freshSet, and stop when the set has more thankletters. Each start can scan most of the string, so this isO(n^2), and every start rebuilds the set from nothing. - Sliding the window would avoid the rebuilds, but a
Setcan't handle a character leaving. If"p"leaves the window, is there another"p"still inside? A set only knows yes or no, not how many, so you can't tell whether to delete it. - So count instead. A
Mapfrom each character to how many copies are in the window answers that question: when a character leaves, take one from its count, and delete the key only when the count reaches zero. Thencounts.sizeis always the number of different characters in the window. - With that, the window can grow and shrink like any variable window: add
s[right], and whilecounts.size > k, removes[left]and moveleftforward. Then measure. That loop is the sliding window with a frequency map.
How it works
- Create an empty
countsmap and setleft = 0andbest = 0. - Move
rightacross the string. Add one to the count ofs[right]. - While the window breaks the rule (here,
counts.size > k), removes[left]: take one from its count, delete the key at zero, and moveleftright. - The window
left..rightnow follows the rule. Compare its size,right - left + 1, withbest. - After the loop, return
best.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function longestWithKDistinct(s, k) {const counts = new Map();let left = 0;let best = 0;for (let right = 0; right < s.length; right++) {const ch = s[right];counts.set(ch, (counts.get(ch) ?? 0) + 1);while (counts.size > k) {const out = s[left];counts.set(out, counts.get(out) - 1);if (counts.get(out) === 0) counts.delete(out);left++;}best = Math.max(best, right - left + 1);}return best;}
How fast it is
| Time | O(n) |
|---|---|
| Extra memory | O(k) |
Each character enters and leaves the window at most once, and each map update is constant time on average. The map holds at most k + 1 keys at any moment.
- Best case
O(n)The whole string has at most
kdifferent letters, like"aaaa"withk = 1:leftnever moves, one map update per character. - Average case
O(n)Any string: each character enters the map once and leaves at most once, and each
Mapupdate is constant time on average. - Worst case
O(n)Letters change constantly, like
"abcabc"withk = 1: almost every character is removed right after it's added. Still at most2nupdates.
Common mistakes
- Leaving keys with a count of
0in the map, socounts.sizeis too big and the window shrinks more than it should. - Using a plain object and
Object.keys(counts).lengthinside the loop. Building the key list each time isO(k)per step; aMapwithsizeis constant time. - Comparing two whole maps on every slide for anagram questions. It works but costs
O(alphabet)each step; track amatchedcounter instead. - Forgetting that
s[left]must be read beforeleft++. Moving first removes the wrong character.
When to use it
Look for these clues in a problem:
- The question is about a substring or subarray and talks about distinct values, repeats, or how many of each kind.
- It says "at most
kdifferent", "no repeating", "contains all the letters of", or "a rearrangement of". - You need counts per value inside the window, not just a total.
- The alphabet is small (letters) or values can be keys in a
Map.
Check yourself
Answer each one in your head first, then open it.
Why can't a Set replace the map?
When a character leaves, a Set can't tell whether another copy is still in the window. Counts can: delete the key only when its count reaches 0.
What goes wrong if you don't delete keys whose count reaches 0?
counts.size keeps counting letters that are no longer in the window, so the window looks like it breaks the rule and shrinks more than it should.
In the trace, why did adding r at right = 5 remove 4 characters?
The window had three ps and two qs. counts.size only drops when one letter's count reaches 0, and the last p was at index 3, so left had to pass indexes 0 to 3.
At most how many keys can counts hold at once?
k + 1. Adding one character can push the size to k + 1, and the while loop brings it back to k before the next character is added.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 11 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingmedium
Go further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
The loop keeps two promises. First, counts holds exactly the characters of s[left..right], each with its number of copies, and no key has a count of 0. Adding one at right and taking one away at left, deleting keys that reach 0, keeps this true at every step. So counts.size is always the number of different characters in the window.
Second, after the while loop, left is the smallest start whose window up to right has at most k different characters. This works because removing characters from the left can never add a new letter, so if a window has too many letters, every bigger window containing it does too. A start skipped for one right stays too early for every later right.
So for each right, left..right is the longest valid window ending there, and best keeps the largest of them. Every valid window ends somewhere, so best is the answer. Each character is added once and removed at most once, so the loop finishes after at most 2n map updates.
Going deeper
3 more ideas
Why deleting zero counts matters
counts.size counts keys, not copies. If a character's count drops to 0 and you leave the key in the map, counts.size still counts it as inside the window, and the window looks like it has more distinct characters than it does. Deleting the key at zero keeps counts.size equal to the number of different characters in the window.
Why it takes O(n) time
Each character is added to the map once, when right reaches it, and removed at most once, when left passes it. Each map update takes constant time on average. The map holds at most one key per distinct character in the window, so memory is O(k), or the alphabet size.
Fixed-size windows with a frequency map
To find every place where a rearrangement of a word appears, slide a window of the word's length and compare its counts with the word's counts. Instead of comparing the whole map each step, keep a number matched of letters whose counts agree, and update it only for the letter that enters and the letter that leaves.
Work it by hand
Every step in a table, with why
Find the longest part with at most 2 different letters. One step has to remove four characters, and only the last removal deletes a key.
s = "ppqpqrrqr", k = 2
| Step | right | ch | Removed | left | counts | best | Why |
|---|---|---|---|---|---|---|---|
| 1 | 0 | p | - | 0 | {p: 1} | 1 | The first character enters. One letter, window size 1. |
| 2 | 1 | p | - | 0 | {p: 2} | 2 | A second p: the count goes up, but counts.size stays 1. |
| 3 | 2 | q | - | 0 | {p: 2, q: 1} | 3 | Two different letters, which is allowed. |
| 4 | 3 | p | - | 0 | {p: 3, q: 1} | 4 | Still two letters. The window is "ppqp". |
| 5 | 4 | q | - | 0 | {p: 3, q: 2} | 5 | Window "ppqpq", size 5, the best so far. |
| 6 | 5 | r | p, p, q, p | 4 | {q: 1, r: 1} | 5 | r makes 3 letters. Removing p (2 left), p (1 left), and q (1 left) doesn't help; the 4th removal takes the last p, its count hits 0, the key is deleted, and counts.size is 2. |
| 7 | 6 | r | - | 4 | {q: 1, r: 2} | 5 | Window "qrr", size 3. |
| 8 | 7 | q | - | 4 | {q: 2, r: 2} | 5 | Window "qrrq", size 4. |
| 9 | 8 | r | - | 4 | {q: 2, r: 3} | 5 | Window "qrrqr", size 5, which ties best. |
It returns 5. Both "ppqpq" and "qrrqr" have 5 characters and 2 letters. The loop ends when right passes the last index.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Sliding window with a frequency map | O(n) | O(k) | The rule is about which values are in a contiguous run, such as distinct letters, repeats, or matching counts. |
Every start with a fresh Set | O(n^2) | O(k) | The string is short, or you want a simple reference version to test against. |
| Frequency map as an array of 26 counts | O(n) | O(1) | The input is only lowercase English letters. Index with ch.charCodeAt(0) - 97, and track the number of non-zero counts yourself instead of counts.size. |
Extra memory: O(k): the map holds at most k + 1 keys, and only for an instant before the shrink step. For letters only, that's at most the alphabet size.
How the steps grow
- Sliding window with a frequency map, Frequency map as an array of 26 counts
O(n) - Every start with a fresh
SetO(n^2)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Sliding window with a frequency map | 10 | 1,000 | 1 million |
Every start with a fresh Set | 100 | 1 million | 1 trillion |
| Frequency map as an array of 26 counts | 10 | 1,000 | 1 million |
Inputs that need care
5 tricky inputs
- An empty string:
s = "" - The loop doesn't run and it returns
0. k = 0- Every new character makes
counts.size1, which is over 0, so it is removed at once and the window is always empty. It returns0. kat least the number of different letters:s = "abcab",k = 5- The rule is never broken, nothing is removed, and it returns the whole length,
5. - Upper and lower case:
s = "aA",k = 1 "a"and"A"are different keys, so it returns1. Lowercase the string first if case shouldn't matter.- Emoji:
s = "😀😀",k = 1 s[right]reads UTF-16 code units, and each emoji is two of them, sos.lengthis 4 and the map sees two different halves. It returns1instead of 2. Loop overArray.from(s)to work with whole characters.
Variations and follow-ups
Twists interviewers add
Longest part with no repeats
Shrink while the character just added has a count above 1, instead of while counts.size > k. A plain object or a last-seen index map can make it even shorter: jump left straight past the earlier copy.
Finding rearrangements of a word
Slide a fixed window the length of the word and keep a number matched of letters whose window count equals the word's count. Update matched only for the letter that enters and the letter that leaves, and record a hit when it equals the number of distinct letters in the word.
Shortest window containing every letter of a word
Grow until the window holds every needed letter with enough copies, tracked with a need map and a counter, then shrink from the left while it still does, recording the shortest. This is the "shortest valid window" form of the same pattern.
At most k changes
For the longest part you can make all one letter by changing at most k characters, keep counts and the highest count seen. The window is valid while its size minus that highest count is at most k.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "substring" or "subarray" plus words about contents: "at most
kdistinct", "no repeating", "contains all characters of", or "anagram of". - Say the two jobs of the map out loud: counts tell you whether a character is still inside after one copy leaves, and deleting at zero keeps
counts.sizeequal to the number of distinct values. - Test by hand:
k = 0, a string with fewer thankdistinct letters, and a step where the left edge has to pass several copies of one letter before a key is deleted, like the 4 removals in the trace. - Expect the follow-up "what if the input is only lowercase letters?". The answer: use an array of 26 counts and keep your own count of non-zero entries, which avoids hashing and makes space
O(1).
Where frontend code uses it
The same idea in real UI code
- Autocomplete and spell-check features compare letter counts in a moving part of the text to find rearranged words.
- Analytics code that finds the longest part of a session where a user touched at most two different features counts feature ids in a sliding window.
- Text editors and linters that flag repeated words or characters within a short stretch of text track recent counts in a window.