Find All Anagrams in a String – Brute-Force to Optimal Solution
If you're trying to find all anagrams of a pattern p
in a string s
, you’re working with one of the most classic sliding window problems in string algorithms.
As engineers who work with high-performance text processing and data streaming systems, we’ve solved this problem hundreds of times, and here’s how you can go from brute-force to blazing fast solutions step-by-step.
What’s the Problem?
You're given:
-
A string
s
(the haystack) -
A pattern
p
(the needle)
You want to return all the starting indices in s
where an anagram of p
begins.
Example:
Input: s = "cbaebabacd", p = "abc"
Output: [0, 6]
Explanation: Anagrams "cba" and "bac" start at indices 0 and 6.
Step 1 – Brute Force (Too Slow, Don't Use in Production)
Let’s start with the most basic idea:
-
Slide over every possible substring in
s
. -
Check if it’s an anagram of
p
.
def is_anagram(res, dp):
for i in dp:
if res.count(i) != dp[i]:
return False
return len(res) == sum(dp.values())
def all_anagrams(s: str, p: str):
dp = {}
for i in p:
dp[i] = dp.get(i, 0) + 1
fin = []
for i in range(len(s)):
test = s[i:i + len(p)]
if len(test) != len(p):
continue
if is_anagram(test, dp):
fin.append(i)
return fin
print("Expected:[0,1,2], Actual:", all_anagrams("abab", "ab"))
print("Expected:[0,6], Actual:", all_anagrams("cbaebabacd", "abc"))
print("Expected:[99999], Actual:", all_anagrams("a"*10**5 + "b", "ab"))
print("Expected:many results, Actual:", all_anagrams("abcde"*200000, "edcba"))
print("Expected:many results, Actual:", all_anagrams("abcd"*250000, "dcba"))
Why It’s Bad:
-
Time Complexity: O(N * M), where N is the length of
s
and M is the length ofp
. -
Space Complexity: O(M) for temporary slices and frequency maps.
-
res.count()
is called repeatedly → super slow for big inputs.
Step 2 – Smarter Brute Force (Still Not Enough)
What’s the improvement?
In the brute-force solution from Step 1, we:
-
Extract every possible substring of any length (which was too much).
-
Count each character in the substring from scratch.
In Step 2, we:
-
Only look at substrings of length equal to
len(p)
(because anagrams must be same length). -
Precompute the frequency of
p
once. -
Avoid checking substrings of wrong length.
We still check every substring of length len(p)
, but now we only compare frequencies.
Key Insight:
Every anagram of p
must:
-
Have the exact same length
-
Have the same count of each character
So, we can:
-
Build a frequency map for
p
-
Slide through
s
using substrings of same length -
Build a frequency map for each substring
-
Compare the two maps
Code: Smarter Brute Force
from typing import List
from collections import Counter
def smarter_brute_force(s: str, p: str) -> List[int]:
"""
Finds all anagrams of p in s using substring slicing + frequency comparison.
This is better than naive brute-force but still not optimal.
"""
if len(p) > len(s):
return []
result = []
p_len = len(p)
p_count = Counter(p)
for i in range(len(s) - p_len + 1):
window = s[i:i + p_len]
window_count = Counter(window)
if window_count == p_count:
result.append(i)
return result
Time and Space Complexity:
-
Time:
-
Creating
p_count
: O(M) where M = len(p) -
For each of N−M+1 windows:
-
Create window string: O(M)
-
Create Counter for window: O(M)
-
Compare two Counters: O(M)
-
Total: O((N−M+1) × M) ⇒ O(N × M)
-
-
Space: O(M) for
p_count
, O(M) per window
Step 3 – The Optimal Solution Using Sliding Window + Hash Map
Let’s optimize this like a pro.
Use the Sliding Window pattern and two hash maps (or arrays if limited to lowercase letters) to track the frequency of characters in:
-
p
-
current window in
s
We slide a window of length len(p)
through s
, updating the counts on the fly.
from typing import List
from collections import Counter
def find_anagrams(s: str, p: str) -> List[int]:
"""
Returns start indices of all anagrams of 'p' in string 's'.
"""
if len(p) > len(s):
return []
p_count = Counter(p) # Frequency map of pattern
s_count = Counter(s[:len(p)]) # Frequency map of first window
result = []
if s_count == p_count:
result.append(0)
for i in range(len(p), len(s)):
s_count[s[i]] += 1 # include new character to window
s_count[s[i - len(p)]] -= 1 # remove old character from window
if s_count[s[i - len(p)]] == 0:
del s_count[s[i - len(p)]]
if s_count == p_count:
result.append(i - len(p) + 1)
return result
Let’s Break It Down Like You’re Two
-
You count how many letters are in your toy box (that’s the pattern
p
). -
You move a window across your bookshelf (string
s
) the same size as your toy box. -
Every time your shelf window has the same toys (letters) as your toy box, you shout “Match!”
-
You write down the shelf number.
What Makes This Fast?
-
Pattern Used: Sliding Window + Hash Map comparison
-
Data Structures:
collections.Counter
(or a fixed-size array ifs
only has lowercase letters) -
Time Complexity: O(N), where N is the length of
s
-
Space Complexity: O(1) if alphabet size is fixed (e.g. 26 lowercase letters), otherwise O(K) where K = size of character set
How to Recognize This Pattern
-
The problem talks about substrings or windows of a fixed size.
-
You’re comparing frequencies or counts of characters or elements.
-
The pattern is looking for repeated matching substrings or anagrams.
If you’re counting things in a sliding range, you likely need a sliding window.
How Could I Derive This On My Own?
-
Think about the brute-force way first: slice every possible substring and check it.
-
Ask: Is there any unnecessary repetition?
-
Look for things you can compute once and update slightly on each move.
-
When you see substring comparisons of same length think sliding window.
-
If you see you’re comparing character makeup think frequency maps.
What You Should Do Next
You’ve just learned how to take a brute-force algorithm and make it super efficient using sliding windows and hash maps.
Here’s what to try next:
-
Solve variations: Find all permutations in string, Longest substring with same characters, Minimum window substring
-
Try replacing
Counter
with arrays for optimization -
Try writing your own pattern recognizer: every time a problem asks for repeated substrings or fixed-length patterns, pause and ask yourself: Sliding Window?
Want More Pattern-Based Solutions?
Start building your own “pattern toolbox” — subscribe to get free deep-dives on 50+ algorithm patterns with code, visuals, and real-world analogies.
Join the Discussion
No comments yet. Be the first to share your thoughts!