Sliding Window with Frequency Map
Slide a window over a string while a hash map tracks character counts and a "formed" counter says how many required characters are satisfied — the engine behind anagram search and minimum window substring.
Overview
Many substring problems ask whether a window contains a required multiset of characters: exactly the letters of p (anagram), or at least the letters of t (Minimum Window Substring). Checking a window by comparing two frequency tables costs O(Σ) per window, where Σ is the alphabet size; over n windows that is O(n·Σ), and with a naive recount it would be O(n·m).
The frequency-window technique keeps a need map (counts required from the pattern) and a have/window map (counts currently inside the window), plus a single integer formed: how many *distinct* characters currently meet their required count. Every add or remove touches one character and can change formed by at most one, so the "is this window valid?" question becomes formed == required in O(1). Combined with a fixed window (anagrams) or a variable window (minimum window substring), this gives O(n + m) overall.
Intuition
A mental model before the formal terms.
You are a shopkeeper who needs a specific list of items — 2 apples, 1 banana. As customers file through a corridor (the window), you keep a checklist: each item type is either "fulfilled" or "short". Rather than recounting the whole corridor every time someone enters or leaves, you only update the single line for that person's item and adjust the count of fulfilled lines. When *all* lines are fulfilled, the corridor is a valid window; then you start letting people leave from the front to see how short the corridor can be while staying fulfilled.
How it works
- Build
need:need[c]= number of timescoccurs in the patternt. Letrequired = |need|, the number of distinct characters. - Initialize
window(empty map),formed = 0,l = 0. - For each
r: letc = s[r]. Incrementwindow[c]. Ifcis inneedandwindow[c] == need[c], incrementformed— this character just became satisfied. - Fixed-width variant (anagrams): if the window exceeds
|t|, removes[l](decrementingformedif that character drops below its requirement) andl++. When the width is exactly|t|andformed == required, recordlas a match. - Variable-width variant (minimum window): while
formed == required, the window is valid — record it if it is the shortest so far, then removes[l](decrementingformedifwindow[c]falls belowneed[c]) andl++to try a shorter one. - Return the collected matches or the best
[l, r]range.
Why it works
Invariant: window is exactly the character multiset of s[l..r], and formed equals the number of characters c with window[c] ≥ need[c]. Both are maintained incrementally: adding c can only push window[c] from need[c] − 1 to need[c] (formed++), and removing c can only push it from need[c] to need[c] − 1 (formed−−). Any other change leaves the satisfied/unsatisfied status of c unchanged, and no other character is affected.
formed == required is therefore equivalent to "the window contains at least every required character in sufficient quantity". For a window of width exactly |t|, "at least" forces "exactly", which is the anagram condition.
The variable-width shrink is safe by the same monotonicity argument as Sliding Window (Variable Size): containment of a multiset is monotone under inclusion. Once s[l..r] is valid, every shorter window ending at r with a larger l is tried; once shrinking makes it invalid, no window starting before the new l and ending at any r' > r can be the *minimum*, because it strictly contains a window that was already recorded.
Each index enters and leaves the window once, and each entry/exit is O(1) with a hash map (or an array of size Σ), giving O(n + m) total.
Recognition
How to tell a problem wants this.
- "Find all anagrams of
pins", "doess2contain a permutation ofs1", "substring with the same character counts". - "Minimum window substring that contains all characters of
t", "smallest substring containing all", "contains every character at least as many times as". - "Substring with concatenation of all words" — the same idea with word-sized tokens instead of characters.
- The pattern has a small alphabet (
a–z, ASCII) and the solution must be linear:|s|, |t| ≤ 10^5. - You start writing
sorted(window) == sorted(p)inside a loop — that is theO(n·m log m)version this technique replaces.
Interactive visualization
Play, step, change the input. ← → and space work too.
1l = 0, best = 0, freq = {}2for r in 0 .. n-1:3 freq[s[r]] += 14 while len(freq) > k:5 freq[s[l]] -= 1; if freq[s[l]] == 0: delete freq[s[l]]6 l += 17 best = max(best, r - l + 1)8return bestPseudocode
1need = counts(t); required = |need|2window = {}, formed = 0, l = 0, best = (inf, -1, -1)3for r in 0..n-1:4 c = s[r]; window[c] += 15 if c in need and window[c] == need[c]: formed += 16 while formed == required: # window [l, r] is valid7 if r - l + 1 < best.len: best = (r - l + 1, l, r)8 d = s[l]; window[d] -= 19 if d in need and window[d] < need[d]: formed -= 110 l = l + 111return s[best.l .. best.r] or ""Implementations
1# Minimum Window Substring: shortest substring of s containing every character of t2from collections import Counter, defaultdict3 4 5def min_window(s: str, t: str) -> str:6 if not s or not t:7 return ""81 · Build the need table and count distinct required characters9 need = Counter(t)10 required = len(need)112 · Window state: formed = distinct characters currently satisfied12 window: defaultdict[str, int] = defaultdict(int)13 formed = 014 l = 015 best_len, best_l = float("inf"), 0163 · Expand: add s[r] and update formed when a character becomes satisfied17 for r, c in enumerate(s):18 window[c] += 119 if c in need and window[c] == need[c]:20 formed += 1214 · Shrink while valid: record the window, then drop s[l]22 while formed == required:23 if r - l + 1 < best_len:24 best_len, best_l = r - l + 1, l25 d = s[l]26 window[d] -= 127 if d in need and window[d] < need[d]:28 formed -= 129 l += 1305 · Return the shortest window found (or empty)31 return "" if best_len == float("inf") else s[best_l : best_l + int(best_len)]Counter(t)builds the need table in one call;len(need)is the number of distinct required characters.defaultdict(int)for the window meanswindow[c] += 1works without a membership check.enumerate(s)gives(r, c)directly — nos[r]indexing for the entering character.formedis bumped only whenwindow[c] == need[c]right after the increment.- The result slice
s[best_l : best_l + best_len]copies the substring once at the end.
The final slice copies O(best_len) characters — unavoidable in Python since strings are immutable, but done once.
Countersupportsneed - windowandnot (need - window)as a validity test, but that is O(Σ) per check; theformedcounter keeps it O(1).defaultdict(int)vsCounterfor the window: both work;Counteraddsmost_commonand arithmetic,defaultdictis marginally faster for plain increments.float("inf")is the idiomatic sentinel;int(best_len)converts back before slicing because slice bounds must be integers.
- Using a plain
dictand forgettingwindow.get(c, 0)on the first increment (KeyError). - Testing validity with
Counter(s[l:r+1]) >= needinside the loop — O(window) per step. - Slicing with
s[best_l:best_r]whenbest_rwas stored inclusive.
- Frequency table choice: C++
std::array<int,128>(or<int,26>) is fastest for ASCII,unordered_mapfor wide alphabets; JS/TSMap(orInt32Array(128)for speed); PythonCounterforneedanddefaultdict(int)forwindow. - Character iteration: Python and JS
for...ofiterate code points, but JSs[r]indexes UTF-16 code units; C++std::stringis bytes — signedcharmust be cast tounsigned charbefore indexing a table. - Substring extraction: C++
substr(start, length), JS/TSslice(start, end), Pythons[start:end]— mixing the length/end conventions is a classic off-by-one. - Sentinel for "no window": C++
INT_MAX(exact), JS/TS/PythonInfinity/float("inf")(a float, needs conversion before use as an index in Python).
Complexity
n = |s|, m = |t|, Σ = alphabet size. With a fixed-size int array for ASCII the constant is tiny; a hash map handles Unicode.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- The window must contain (exactly or at least) a required multiset of characters or tokens.
- Anagram / permutation-in-string search with a fixed-width window.
- Minimum window containing all of
twith a variable-width window. - Any "count of distinct satisfied requirements" check that must be
O(1)per step.
- The condition is about order, not counts (find
tas a substring, not an anagram) — use Knuth–Morris–Pratt (KMP), Z-Algorithm, or Rabin–Karp. - The requirement is not monotone under inclusion (e.g. "exactly these characters and no others" over a variable width) — reformulate or enumerate.
- Very large alphabets where comparing two maps once per window is fine because
nis tiny. - Subsequence containment ("is
ta subsequence ofs") — a single greedy pointer suffices.
Alternatives
Common mistakes
- Incrementing
formedon every add of a needed character instead of only whenwindow[c]becomes equal toneed[c]— overcounts when a character appears more than required. - Decrementing
formedwhenwindow[c]drops fromneed[c] + 1toneed[c]— that character is still satisfied. - In the anagram variant, comparing
formed == requiredbefore the window has reached width|p|. - Off-by-one in the outgoing index:
s[r − m]leaves whenr ≥ m, nots[r − m + 1]. - Returning the last valid window instead of the shortest, or slicing with
[bestL, bestR)whenbestRis inclusive. - Treating
tas a set (losing multiplicity) —t = "AAB"needs twoAs.
Interview patterns
- Minimum Window Substring — the canonical
need/formedtemplate. - Find All Anagrams in a String and Permutation in String — fixed width plus counts.
- Substring with Concatenation of All Words — tokens are words; run the window once per offset
0..wordLen−1. - Longest Substring with At Most K Distinct — the map size is the constraint; see Sliding Window (Variable Size).
- Group Anagrams uses the same frequency signature idea but as a hash key rather than a window.
- Smallest range covering elements from
klists — same "formed" counter over list ids with a heap.
- Recognizing the approach from an array and a targetIntermediate
- Deciding whether O(n²) can be improvedIntermediate
- Hash map or array?Beginner
- Average case versus worst caseIntermediate
- Minimum Size Subarray SumIntermediate
- Two SumBeginner
- Longest Substring Without Repeating CharactersIntermediate