Every place an anagram begins
Given lowercase strings s and p, return every index i such
that the substring of s starting at i with length |p| is an anagram of
p: the same letters, each the same number of times. Return the indices in increasing order.
Examples
| Input | Output | Why |
|---|---|---|
s = "cbaebabacd", p = "abc" | [0, 6] | "cba" at 0 and "bac" at 6. |
s = "abab", p = "ab" | [0, 1, 2] | "ab", "ba", "ab". |
s = "hello", p = "xyz" | [] | No window matches. |
Constraints
- 1 ≤ |s|, |p| ≤ 3 × 10⁴
- s and p are lowercase English letters
Hints
Hint 1
Two strings are anagrams exactly when their letter counts agree. Every window has the same length as p.
Hint 2
Moving the window changes two counts. Comparing all 26 counts each time works; can you avoid even that?
Hint 3
Keep need[c] = count of c in p minus count of c in the window, and the number of letters where need[c] ≠ 0. The window matches when that number is 0.
Where people lose marks
- Returning early when p is longer than s is correct, but forgetting it makes some languages index out of range.
- Updating
offafter changing the count instead of comparing the old and new values. - Sorting every window: O(|s| · |p| log |p|).
Dry run on "cbaebabacd","abc"
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 10sc0lo/hib1a2e3b4a5b6a7c8d9off2found[]
'c' enters. The window is not full yet.
-
Step 2 / 10sc0lob1hia2e3b4a5b6a7c8d9off1found[]
'b' enters. The window is not full yet.
-
Step 3 / 10sc0lob1a2hie3b4a5b6a7c8d9off0found[0]
'a' enters. Every count matches (off = 0): "cba" is an anagram, so record 0.
-
Step 4 / 10sc0b1loa2e3hib4a5b6a7c8d9off2found[0]
'e' enters. 'c' leaves. 2 letters still differ.
-
Step 5 / 10sc0b1a2loe3b4hia5b6a7c8d9off2found[0]
'b' enters. 'b' leaves. 2 letters still differ.
-
Step 6 / 10sc0b1a2e3lob4a5hib6a7c8d9off2found[0]
'a' enters. 'a' leaves. 2 letters still differ.
-
Step 7 / 10sc0b1a2e3b4loa5b6hia7c8d9off2found[0]
'b' enters. 'e' leaves. 2 letters still differ.
-
Step 8 / 10sc0b1a2e3b4a5lob6a7hic8d9off2found[0]
'a' enters. 'b' leaves. 2 letters still differ.
-
Step 9 / 10sc0b1a2e3b4a5b6loa7c8hid9off0found[0, 6]
'c' enters. 'a' leaves. Every count matches (off = 0): "bac" is an anagram, so record 6.
-
Step 10 / 10sc0b1a2e3b4a5b6a7loc8d9hioff2found[0, 6]
'd' enters. 'b' leaves. 2 letters still differ.
How to think about it
The window has the length of p and slides one place at a time, so this is a fixed window
whose state is a table of letter counts. Store the difference need[c] between the pattern's count
and the window's. A letter entering the window lowers its need by one; a letter leaving raises it.
Comparing all 26 entries after each step would already be linear in the length of s, with a
factor of 26. The tidier way keeps one more number, off: how many letters have a nonzero
need. Each update changes one entry, so off changes by at most one: it goes down when
an entry reaches zero and up when it leaves zero. The window is an anagram exactly when off is
zero.
Complexity. Time O(|s| + |p|) · Space O(1), 26 counts
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def anagram_starts(s, p):
k = len(p)
need = [0] * 26 # count in p minus count in the window
for c in p:
need[ord(c) - 97] += 1
off = sum(1 for x in need if x) # letters whose counts still differ
def bump(c, d):
nonlocal off
i = ord(c) - 97
if need[i] == 0:
off += 1
need[i] += d
if need[i] == 0:
off -= 1
out = []
for hi, c in enumerate(s):
bump(c, -1) # c enters the window
if hi >= k:
bump(s[hi - k], +1) # the letter k places back leaves
if hi >= k - 1 and off == 0:
out.append(hi - k + 1)
return out#include <string>
#include <vector>
#include <array>
std::vector<int> solve(const std::string& s, const std::string& p) {
int k = (int)p.size();
std::array<int, 26> need{}; // count in p minus count in the window
for (char c : p) need[c - 'a']++;
int off = 0;
for (int x : need) off += x != 0; // letters whose counts still differ
auto bump = [&](char c, int d) {
int& x = need[c - 'a'];
if (x == 0) ++off;
x += d;
if (x == 0) --off;
};
std::vector<int> out;
for (int hi = 0; hi < (int)s.size(); ++hi) {
bump(s[hi], -1); // enters
if (hi >= k) bump(s[hi - k], +1); // leaves
if (hi >= k - 1 && off == 0) out.push_back(hi - k + 1);
}
return out;
}import java.util.*;
class Solution {
public List<Integer> anagramStarts(String s, String p) {
int k = p.length();
int[] need = new int[26]; // count in p minus count in the window
for (char c : p.toCharArray()) need[c - 'a']++;
int off = 0;
for (int x : need) if (x != 0) off++; // letters whose counts still differ
List<Integer> out = new ArrayList<>();
for (int hi = 0; hi < s.length(); hi++) {
int i = s.charAt(hi) - 'a'; // enters
if (need[i] == 0) off++;
if (--need[i] == 0) off--;
if (hi >= k) { // leaves
int j = s.charAt(hi - k) - 'a';
if (need[j] == 0) off++;
if (++need[j] == 0) off--;
}
if (hi >= k - 1 && off == 0) out.add(hi - k + 1);
}
return out;
}
}function anagramStarts(s, p) {
const k = p.length, need = new Array(26).fill(0); // count in p minus count in the window
for (const c of p) need[c.charCodeAt(0) - 97]++;
let off = need.filter(x => x !== 0).length; // letters whose counts still differ
const bump = (c, d) => {
const i = c.charCodeAt(0) - 97;
if (need[i] === 0) off++;
need[i] += d;
if (need[i] === 0) off--;
};
const out = [];
for (let hi = 0; hi < s.length; hi++) {
bump(s[hi], -1); // enters
if (hi >= k) bump(s[hi - k], +1); // leaves
if (hi >= k - 1 && off === 0) out.push(hi - k + 1);
}
return out;
}fn solve(s: &str, p: &str) -> Vec<usize> {
let (s, p) = (s.as_bytes(), p.as_bytes());
let k = p.len();
let mut need = [0i32; 26]; // count in p minus count in the window
for &c in p { need[(c - b'a') as usize] += 1; }
let mut off = need.iter().filter(|&&x| x != 0).count() as i32;
let mut bump = |need: &mut [i32; 26], c: u8, d: i32| {
let x = &mut need[(c - b'a') as usize];
if *x == 0 { off += 1; }
*x += d;
if *x == 0 { off -= 1; }
off
};
let mut out = Vec::new();
for hi in 0..s.len() {
let mut o = bump(&mut need, s[hi], -1); // enters
if hi >= k { o = bump(&mut need, s[hi - k], 1); } // leaves
if hi + 1 >= k && o == 0 { out.push(hi + 1 - k); }
}
out
}import qualified Data.Map.Strict as M
-- Fold over the letters, each paired with the letter leaving the window (if any), carrying
-- need: pattern count minus window count, with zeros removed. The window is an anagram
-- exactly when the map is empty.
solve :: String -> String -> [Int]
solve s p = reverse found
where
k = length p
start = M.fromListWith (+) [(c, 1) | c <- p]
bump d c = M.alter (\x -> let v = maybe 0 id x + d in if v == 0 then Nothing else Just v) c
leaving = replicate k Nothing ++ map Just s
(_, found) = foldl step (start, []) (zip3 [0 ..] s leaving)
step (m, acc) (hi, c, out) =
let m' = maybe id (bump 1) out (bump (-1) c m)
acc' = if hi >= k - 1 && M.null m' then (hi - k + 1) : acc else acc
in (m', acc')
Variants to try
- Only whether some anagram occurs (the permutation-in-string question): stop at the first match.
- Unicode text: replace the 26-entry array with a hash map; nothing else changes.
Next problems
Best total over k consecutive days
A window of fixed width slides one place at a time: add the element that enters, subtract the one that leaves.
Open the problem → Easy10 minMost vowels in any k letters
The same fixed window as a running sum, with a count of the letters that matter as its state.
Open the problem → Medium15 minDistinct values in every window
Keep a count per value, not a set: a set cannot tell whether the value leaving still has a copy inside.
Open the problem →Write a solution and run it against the real test table.
Keep going
Best total over k consecutive days
A window of fixed width slides one place at a time: add the element that enters, subtract the one that leaves.
Open the problem → Easy10 minMost vowels in any k letters
The same fixed window as a running sum, with a count of the letters that matter as its state.
Open the problem → Medium15 minDistinct values in every window
Keep a count per value, not a set: a set cannot tell whether the value leaving still has a copy inside.
Open the problem → Sliding windowThe pattern behind it
A window that grows on the right, shrinks on the left, and holds an invariant at all times.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.