Facebook Pixel

438. Find All Anagrams in a String

MediumHash TableStringSliding Window
Leetcode Link

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" and p = "abc", the anagrams of p found in s are "cba" starting at index 0 and "bac" starting at index 6. So the output would be [0, 6].
  • If s = "abab" and p = "ab", the anagrams of p found in s 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Start with a window of size len(p)
  2. Keep track of character frequencies in the current window
  3. 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 string p - this remains constant throughout
  • cnt2 is initialized with the first n-1 characters of s (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:

  1. Add the rightmost character: cnt2[s[i]] += 1 completes the window of size n by adding the character at position i

  2. 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 is i - n + 1, which we add to our result list

  3. 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) where m = len(s), as we visit each character once and Counter comparison is O(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 Evaluator

Example 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) takes O(n) time where n is the length of string p.
  • Creating Counter(s[:n-1]) takes O(n) time.
  • The main loop runs m - n + 1 times (from index n-1 to m-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)
  • 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 most m - 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 to Counter({'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 window
  • p_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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More