438. Find All Anagrams in a String
Problem Description
You are given two strings s
and p
. Your task is to find all the starting positions in string s
where an anagram of string p
begins.
An anagram is a word or phrase formed by rearranging the letters of another word or phrase, using all the original letters exactly once. For example, "abc", "bca", and "cab" are all anagrams of each other.
You need to return a list of all starting indices where such anagrams of p
can be found in s
. The indices can be returned in any order.
For example:
- If
s = "cbaebabacd"
andp = "abc"
, the anagrams ofp
found ins
are "cba" starting at index 0 and "bac" starting at index 6. So the output would be[0, 6]
. - If
s = "abab"
andp = "ab"
, the anagrams ofp
found ins
are "ab" starting at index 0, "ba" starting at index 1, and "ab" starting at index 2. So the output would be[0, 1, 2]
.
The key insight is that you're looking for substrings of s
that have the same length as p
and contain exactly the same characters with the same frequencies as p
, just potentially in a different order.
Intuition
To find all anagrams of p
in string s
, we need to check if substrings of s
have the same character frequencies as p
. The naive approach would be to check every possible substring of length len(p)
in s
, but this would involve repeatedly counting characters for overlapping windows.
The key observation is that consecutive windows of the same size overlap significantly - they differ by only one character. When we slide from one window to the next, we're essentially removing one character from the left and adding one character from the right.
This naturally leads us to a sliding window approach with frequency counting. Instead of recounting all characters for each window, we can:
- Start with a window of size
len(p)
- Keep track of character frequencies in the current window
- As we slide the window one position to the right, update the frequency count by:
- Adding the new character that enters the window on the right
- Removing the character that exits the window on the left
To check if a window is an anagram, we simply compare the frequency map of the current window with the frequency map of p
. If they're identical, we've found an anagram.
The efficiency comes from the fact that updating the frequency map for each new window position takes O(1)
time (just two updates), rather than O(n)
time to recount everything. We're essentially maintaining a "running count" of characters as we slide through the string, which is much more efficient than starting fresh for each position.
Solution Approach
The solution implements a sliding window technique with character frequency counting using Python's Counter
data structure.
Step 1: Initial Setup
m, n = len(s), len(p)
ans = []
if m < n:
return ans
First, we get the lengths of both strings. If s
is shorter than p
, no anagram is possible, so we return an empty list immediately.
Step 2: Create Frequency Maps
cnt1 = Counter(p) cnt2 = Counter(s[: n - 1])
cnt1
stores the character frequencies of stringp
- this remains constant throughoutcnt2
is initialized with the firstn-1
characters ofs
(not the full window yet)
Step 3: Sliding Window Process
for i in range(n - 1, m):
cnt2[s[i]] += 1
if cnt1 == cnt2:
ans.append(i - n + 1)
cnt2[s[i - n + 1]] -= 1
The loop starts from index n-1
and goes through the entire string s
:
-
Add the rightmost character:
cnt2[s[i]] += 1
completes the window of sizen
by adding the character at positioni
-
Check for anagram:
if cnt1 == cnt2
compares the two frequency maps. If they're identical, we've found an anagram. The starting index of this window isi - n + 1
, which we add to our result list -
Remove the leftmost character:
cnt2[s[i - n + 1]] -= 1
prepares for the next iteration by removing the leftmost character of the current window
Why this works:
- The window maintains exactly
n
characters at all times (after the first addition in each iteration) - By adding one character on the right and removing one on the left, we efficiently slide the window
- Python's
Counter
comparison checks if two dictionaries have identical key-value pairs, perfect for anagram detection - The time complexity is
O(m)
wherem = len(s)
, as we visit each character once and Counter comparison isO(1)
for fixed alphabet size
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example with s = "cbaebabacd"
and p = "abc"
.
Initial Setup:
m = 10
(length of s),n = 3
(length of p)cnt1 = {'a': 1, 'b': 1, 'c': 1}
(frequency map of "abc")cnt2 = {'c': 1, 'b': 1}
(frequency map of first 2 characters "cb")
Sliding Window Process:
Iteration 1 (i = 2):
- Add
s[2] = 'a'
:cnt2 = {'c': 1, 'b': 1, 'a': 1}
- Window contains "cba" (indices 0-2)
- Compare:
cnt1 == cnt2
? YES! Both have{'a': 1, 'b': 1, 'c': 1}
- Add starting index:
2 - 3 + 1 = 0
to result - Remove
s[0] = 'c'
:cnt2 = {'c': 0, 'b': 1, 'a': 1}
Iteration 2 (i = 3):
- Add
s[3] = 'e'
:cnt2 = {'c': 0, 'b': 1, 'a': 1, 'e': 1}
- Window contains "bae" (indices 1-3)
- Compare:
cnt1 == cnt2
? NO (cnt2 has 'e', no 'c') - Remove
s[1] = 'b'
:cnt2 = {'c': 0, 'b': 0, 'a': 1, 'e': 1}
Iteration 3 (i = 4):
- Add
s[4] = 'b'
:cnt2 = {'c': 0, 'b': 1, 'a': 1, 'e': 1}
- Window contains "aeb" (indices 2-4)
- Compare:
cnt1 == cnt2
? NO - Remove
s[2] = 'a'
:cnt2 = {'c': 0, 'b': 1, 'a': 0, 'e': 1}
Skip to Iteration 7 (i = 8):
- After iterations 4-6,
cnt2 = {'b': 1, 'a': 1, 'c': 0}
- Add
s[8] = 'c'
:cnt2 = {'b': 1, 'a': 1, 'c': 1}
- Window contains "bac" (indices 6-8)
- Compare:
cnt1 == cnt2
? YES! Both have{'a': 1, 'b': 1, 'c': 1}
- Add starting index:
8 - 3 + 1 = 6
to result - Remove
s[6] = 'b'
:cnt2 = {'b': 0, 'a': 1, 'c': 1}
Final Result: [0, 6]
- anagrams found at indices 0 and 6.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def findAnagrams(self, s: str, p: str) -> List[int]:
6 """
7 Find all start indices of p's anagrams in s.
8 Uses sliding window technique with character frequency counters.
9
10 Args:
11 s: The string to search in
12 p: The pattern string to find anagrams of
13
14 Returns:
15 List of starting indices where anagrams of p are found in s
16 """
17 s_length, p_length = len(s), len(p)
18 result = []
19
20 # Early return if s is shorter than p
21 if s_length < p_length:
22 return result
23
24 # Counter for pattern p's character frequencies
25 pattern_counter = Counter(p)
26
27 # Initialize window counter with first (p_length - 1) characters of s
28 window_counter = Counter(s[:p_length - 1])
29
30 # Slide the window through string s
31 for i in range(p_length - 1, s_length):
32 # Add the rightmost character to the window
33 window_counter[s[i]] += 1
34
35 # Check if current window is an anagram of p
36 if pattern_counter == window_counter:
37 # Add the starting index of this window
38 result.append(i - p_length + 1)
39
40 # Remove the leftmost character from the window
41 # Prepare for next iteration
42 left_char = s[i - p_length + 1]
43 window_counter[left_char] -= 1
44
45 # Remove the character from counter if its count becomes 0
46 if window_counter[left_char] == 0:
47 del window_counter[left_char]
48
49 return result
50
1class Solution {
2 public List<Integer> findAnagrams(String s, String p) {
3 int sLength = s.length();
4 int pLength = p.length();
5 List<Integer> result = new ArrayList<>();
6
7 // If s is shorter than p, no anagrams possible
8 if (sLength < pLength) {
9 return result;
10 }
11
12 // Count frequency of each character in pattern p
13 int[] patternFreq = new int[26];
14 for (int i = 0; i < pLength; i++) {
15 patternFreq[p.charAt(i) - 'a']++;
16 }
17
18 // Initialize sliding window frequency array with first (pLength - 1) characters
19 int[] windowFreq = new int[26];
20 for (int i = 0; i < pLength - 1; i++) {
21 windowFreq[s.charAt(i) - 'a']++;
22 }
23
24 // Slide the window through string s
25 for (int i = pLength - 1; i < sLength; i++) {
26 // Add the rightmost character to the window
27 windowFreq[s.charAt(i) - 'a']++;
28
29 // Check if current window is an anagram of p
30 if (Arrays.equals(patternFreq, windowFreq)) {
31 // Add the starting index of the current window
32 result.add(i - pLength + 1);
33 }
34
35 // Remove the leftmost character from the window for next iteration
36 windowFreq[s.charAt(i - pLength + 1) - 'a']--;
37 }
38
39 return result;
40 }
41}
42
1class Solution {
2public:
3 vector<int> findAnagrams(string s, string p) {
4 int sLength = s.size();
5 int pLength = p.size();
6 vector<int> result;
7
8 // Early return if source string is shorter than pattern
9 if (sLength < pLength) {
10 return result;
11 }
12
13 // Count frequency of each character in pattern string
14 vector<int> patternCount(26, 0);
15 for (char& ch : p) {
16 patternCount[ch - 'a']++;
17 }
18
19 // Initialize sliding window with first (pLength - 1) characters
20 vector<int> windowCount(26, 0);
21 for (int i = 0; i < pLength - 1; i++) {
22 windowCount[s[i] - 'a']++;
23 }
24
25 // Slide the window through the string
26 for (int i = pLength - 1; i < sLength; i++) {
27 // Add the rightmost character to the window
28 windowCount[s[i] - 'a']++;
29
30 // Check if current window is an anagram of pattern
31 if (windowCount == patternCount) {
32 result.push_back(i - pLength + 1); // Store starting index
33 }
34
35 // Remove the leftmost character from the window
36 windowCount[s[i - pLength + 1] - 'a']--;
37 }
38
39 return result;
40 }
41};
42
1/**
2 * Finds all starting indices of anagrams of pattern string p in string s
3 * Uses sliding window technique with character frequency counting
4 * @param s - The source string to search in
5 * @param p - The pattern string whose anagrams we're looking for
6 * @returns Array of starting indices where anagrams are found
7 */
8function findAnagrams(s: string, p: string): number[] {
9 const sourceLength: number = s.length;
10 const patternLength: number = p.length;
11 const result: number[] = [];
12
13 // Early return if source string is shorter than pattern
14 if (sourceLength < patternLength) {
15 return result;
16 }
17
18 // Character frequency arrays for pattern and current window
19 // Using array of size 26 for lowercase English letters
20 const patternFrequency: number[] = new Array(26).fill(0);
21 const windowFrequency: number[] = new Array(26).fill(0);
22
23 /**
24 * Helper function to convert character to array index
25 * Maps 'a' to 0, 'b' to 1, ..., 'z' to 25
26 */
27 const getCharIndex = (char: string): number => {
28 return char.charCodeAt(0) - 'a'.charCodeAt(0);
29 };
30
31 // Count character frequencies in pattern string
32 for (const char of p) {
33 patternFrequency[getCharIndex(char)]++;
34 }
35
36 // Initialize window with first (patternLength - 1) characters
37 for (const char of s.slice(0, patternLength - 1)) {
38 windowFrequency[getCharIndex(char)]++;
39 }
40
41 // Slide the window through the source string
42 for (let i: number = patternLength - 1; i < sourceLength; i++) {
43 // Add the rightmost character to the window
44 windowFrequency[getCharIndex(s[i])]++;
45
46 // Check if current window is an anagram of pattern
47 if (patternFrequency.toString() === windowFrequency.toString()) {
48 // Add starting index of current window to result
49 result.push(i - patternLength + 1);
50 }
51
52 // Remove the leftmost character from the window for next iteration
53 windowFrequency[getCharIndex(s[i - patternLength + 1])]--;
54 }
55
56 return result;
57}
58
Time and Space Complexity
Time Complexity: O(m)
where m
is the length of string s
.
- Creating
Counter(p)
takesO(n)
time wheren
is the length of stringp
. - Creating
Counter(s[:n-1])
takesO(n)
time. - The main loop runs
m - n + 1
times (from indexn-1
tom-1
). - Inside the loop:
- Adding/incrementing a character to
cnt2
:O(1)
- Comparing two Counter objects
cnt1 == cnt2
:O(1)
on average since both counters have at most 26 keys (lowercase English letters) - Decrementing a character from
cnt2
:O(1)
- Appending to result list:
O(1)
- Adding/incrementing a character to
- Total:
O(n) + O(n) + O(m - n + 1) × O(1) = O(m)
Space Complexity: O(1)
or O(n)
depending on interpretation.
cnt1
stores at most 26 key-value pairs (for lowercase English letters):O(1)
cnt2
stores at most 26 key-value pairs:O(1)
- The result list
ans
can contain at mostm - n + 1
indices in the worst case:O(m - n + 1) = O(m)
- If we consider only auxiliary space (excluding output), the space complexity is
O(1)
since the Counter objects are bounded by the alphabet size. - If we include the output space, the space complexity is
O(m)
in the worst case.
Common Pitfalls
1. Not Cleaning Up Zero-Count Entries in Counter
The Pitfall: When decrementing character counts in the sliding window, leaving entries with zero counts in the Counter can cause incorrect comparisons. For example:
Counter({'a': 1, 'b': 0})
is NOT equal toCounter({'a': 1})
- This leads to missing valid anagrams because the equality check
pattern_counter == window_counter
fails even when the actual character frequencies match
Example of the Bug:
# Buggy version - doesn't remove zero-count entries
for i in range(p_length - 1, s_length):
window_counter[s[i]] += 1
if pattern_counter == window_counter:
result.append(i - p_length + 1)
window_counter[s[i - p_length + 1]] -= 1 # Leaves zero counts!
The Solution: Always remove entries when their count reaches zero:
left_char = s[i - p_length + 1] window_counter[left_char] -= 1 if window_counter[left_char] == 0: del window_counter[left_char] # Critical: remove zero-count entries
2. Incorrect Window Initialization
The Pitfall:
Initializing the window with the full p_length
characters instead of p_length - 1
breaks the sliding window logic:
# Wrong: Starting with full window window_counter = Counter(s[:p_length]) # This is incorrect!
This causes the first iteration to have p_length + 1
characters in the window, leading to incorrect results.
The Solution:
Initialize with p_length - 1
characters, then complete the window in the loop:
# Correct: Start with p_length - 1 characters window_counter = Counter(s[:p_length - 1]) # Then in the loop, add one character to complete the window
3. Off-by-One Errors in Index Calculation
The Pitfall: Incorrectly calculating the starting index of the anagram window:
# Common mistakes: result.append(i - p_length) # Wrong: off by one result.append(i - n) # Wrong: using wrong variable result.append(i) # Wrong: using end index instead of start
The Solution:
The correct formula is i - p_length + 1
where:
i
is the current (rightmost) position in the windowp_length
is the window size- Adding 1 adjusts for the 0-based indexing
Visual Example:
s = "cbaebabacd", p = "abc" (length 3) When i = 2: window is s[0:3] = "cba" Starting index = 2 - 3 + 1 = 0 ✓
The three-steps of Depth First Search are:
- Identify states;
- Draw the state-space tree;
- DFS on the state-space tree.
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!