2273. Find Resultant Array After Removing Anagrams
Problem Description
You have an array of strings called words
where each string contains only lowercase English letters. Your task is to remove consecutive anagram pairs from this array.
The removal process works as follows:
- Look for any position
i
(where0 < i < words.length
) wherewords[i-1]
andwords[i]
are anagrams of each other - When you find such a pair, delete
words[i]
from the array - Continue this process until no more consecutive anagram pairs exist
Two strings are anagrams if one can be formed by rearranging the letters of the other, using all letters exactly once. For example, "dacb"
and "abdc"
are anagrams because they contain the same letters in different orders.
The key insight is that you only remove the second word in each anagram pair (at index i
), keeping the first word (at index i-1
). This means if you have multiple consecutive anagrams like ["abc", "bca", "cab"]
, you would remove "bca"
first (since it's an anagram of "abc"
), then check again and remove "cab"
(since it's now consecutive with "abc"
).
The problem guarantees that regardless of the order in which you perform the removals, you'll always end up with the same final result. Your goal is to return the array after all possible removals have been made.
For example:
- Input:
["abba", "baba", "bbaa", "cd", "cd"]
- After removing
"baba"
(anagram of"abba"
):["abba", "bbaa", "cd", "cd"]
- After removing
"bbaa"
(anagram of"abba"
):["abba", "cd", "cd"]
- After removing the second
"cd"
(anagram of first"cd"
):["abba", "cd"]
- Output:
["abba", "cd"]
Intuition
The key observation is that we need to keep removing consecutive anagram pairs until no more can be removed. At first glance, this might seem like we need to repeatedly scan the array and remove elements, which could be inefficient.
However, let's think about what happens during the removal process. When we remove words[i]
because it's an anagram of words[i-1]
, the element at words[i+1]
now becomes adjacent to words[i-1]
. But here's the crucial insight: if words[i+1]
were an anagram of words[i-1]
, we would need to remove it too. This process continues until we find a word that is NOT an anagram of words[i-1]
.
This means that for any position in the original array, we only keep a word if it's not an anagram of the word we kept immediately before it. In other words, we can solve this problem in a single pass through the array by maintaining only the words that are not anagrams of their immediate predecessor in our result.
The algorithm becomes straightforward:
- Always keep the first word (it has no predecessor to compare with)
- For each subsequent word, check if it's an anagram of the last word we kept
- If it's not an anagram, keep it; otherwise, skip it
This works because removing a word doesn't affect whether future words are anagrams of previously kept words - it only matters whether each word is an anagram of the most recently kept word.
To check if two strings are anagrams, we can either:
- Sort both strings and compare them
- Count the frequency of each character and compare the counts
The solution uses the second approach with a Counter
to track character frequencies, checking if the counts match between the two strings.
Learn more about Sorting patterns.
Solution Approach
The solution implements a single-pass approach using a helper function to check if two strings are anagrams.
Helper Function check(s, t)
:
This function determines if two strings are NOT anagrams (returns True
if they're different, False
if they're anagrams):
-
First, compare the lengths of
s
andt
. If they differ, they cannot be anagrams, so returnTrue
. -
Use a
Counter
to count the frequency of each character in strings
. -
Iterate through each character
c
in stringt
:- Decrement the count for character
c
by 1:cnt[c] -= 1
- If
cnt[c] < 0
, it meanst
has more occurrences of characterc
thans
, so they're not anagrams - returnTrue
- Decrement the count for character
-
If we successfully process all characters in
t
without any count going negative, the strings are anagrams - returnFalse
.
Main Solution:
The main solution uses Python's pairwise
function to elegantly handle consecutive pair comparisons:
return [words[0]] + [t for s, t in pairwise(words) if check(s, t)]
This works as follows:
-
Start with
words[0]
in the result (the first word is always kept since it has no predecessor). -
Use
pairwise(words)
to generate all consecutive pairs(words[i], words[i+1])
from the array. -
For each pair
(s, t)
wheres = words[i]
andt = words[i+1]
:- If
check(s, t)
returnsTrue
(meaning they're NOT anagrams), includet
in the result - If
check(s, t)
returnsFalse
(meaning they ARE anagrams), skipt
- If
This approach effectively filters out any word that is an anagram of its immediate predecessor, achieving the desired result in a single pass with O(n * m)
time complexity, where n
is the number of words and m
is the average length of each word.
The beauty of this solution is that it recognizes we don't need to actually perform removals - we can directly construct the final result by keeping only the words that wouldn't be removed.
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 walk through a small example: words = ["abc", "bca", "xyz", "zyx", "z"]
Step 1: Initialize result with first word
- Result starts with
["abc"]
(first word is always kept)
Step 2: Process consecutive pairs using the check function
For pair ("abc", "bca")
:
- Check if they're anagrams:
- Both have length 3 β
- Count characters in "abc": {'a':1, 'b':1, 'c':1}
- Process "bca":
- 'b': count becomes {'a':1, 'b':0, 'c':1}
- 'c': count becomes {'a':1, 'b':0, 'c':0}
- 'a': count becomes {'a':0, 'b':0, 'c':0}
- No negative counts, so they ARE anagrams
check("abc", "bca")
returnsFalse
β skip "bca"- Result:
["abc"]
For pair ("bca", "xyz")
:
- Even though we skipped "bca", pairwise still generates this pair from the original array
- Check if they're anagrams:
- Both have length 3 β
- Count characters in "bca": {'b':1, 'c':1, 'a':1}
- Process "xyz":
- 'x': not in counter, so count becomes -1
- Count went negative, so they're NOT anagrams
check("bca", "xyz")
returnsTrue
β include "xyz"- Result:
["abc", "xyz"]
For pair ("xyz", "zyx")
:
- Check if they're anagrams:
- Both have length 3 β
- Count characters in "xyz": {'x':1, 'y':1, 'z':1}
- Process "zyx":
- 'z': count becomes {'x':1, 'y':1, 'z':0}
- 'y': count becomes {'x':1, 'y':0, 'z':0}
- 'x': count becomes {'x':0, 'y':0, 'z':0}
- No negative counts, so they ARE anagrams
check("xyz", "zyx")
returnsFalse
β skip "zyx"- Result:
["abc", "xyz"]
For pair ("zyx", "z")
:
- Check if they're anagrams:
- Different lengths (3 vs 1)
check("zyx", "z")
returnsTrue
β include "z"- Result:
["abc", "xyz", "z"]
Final Result: ["abc", "xyz", "z"]
The solution correctly identified and skipped the anagram pairs ("abc"/"bca" and "xyz"/"zyx"), keeping only the non-consecutive anagram words.
Solution Implementation
1from typing import List
2from collections import Counter
3from itertools import pairwise
4
5class Solution:
6 def removeAnagrams(self, words: List[str]) -> List[str]:
7 def is_not_anagram(s: str, t: str) -> bool:
8 """
9 Check if two strings are NOT anagrams of each other.
10 Returns True if they are NOT anagrams, False if they ARE anagrams.
11 """
12 # Different lengths mean they cannot be anagrams
13 if len(s) != len(t):
14 return True
15
16 # Count character frequencies in the first string
17 char_count = Counter(s)
18
19 # Subtract character frequencies from the second string
20 for char in t:
21 char_count[char] -= 1
22 # If any character count goes negative, they're not anagrams
23 if char_count[char] < 0:
24 return True
25
26 # If all counts match, they are anagrams (return False)
27 return False
28
29 # Keep the first word, then add each subsequent word only if it's not an anagram of the previous
30 result = [words[0]]
31 for prev_word, curr_word in pairwise(words):
32 if is_not_anagram(prev_word, curr_word):
33 result.append(curr_word)
34
35 return result
36
1class Solution {
2 /**
3 * Removes consecutive anagrams from the array, keeping only the first occurrence
4 * @param words Array of strings to process
5 * @return List of strings with consecutive anagrams removed
6 */
7 public List<String> removeAnagrams(String[] words) {
8 List<String> result = new ArrayList<>();
9
10 // Always add the first word
11 result.add(words[0]);
12
13 // Check each word against its predecessor
14 for (int i = 1; i < words.length; i++) {
15 // If current word is NOT an anagram of the previous word, add it
16 if (isNotAnagram(words[i - 1], words[i])) {
17 result.add(words[i]);
18 }
19 }
20
21 return result;
22 }
23
24 /**
25 * Checks if two strings are NOT anagrams of each other
26 * @param firstWord First string to compare
27 * @param secondWord Second string to compare
28 * @return true if strings are NOT anagrams, false if they are anagrams
29 */
30 private boolean isNotAnagram(String firstWord, String secondWord) {
31 // Different lengths mean they cannot be anagrams
32 if (firstWord.length() != secondWord.length()) {
33 return true;
34 }
35
36 // Count frequency of each character in the first word
37 int[] characterCount = new int[26];
38 for (int i = 0; i < firstWord.length(); i++) {
39 characterCount[firstWord.charAt(i) - 'a']++;
40 }
41
42 // Decrement count for each character in the second word
43 for (int i = 0; i < secondWord.length(); i++) {
44 // If count goes negative, words have different character frequencies
45 if (--characterCount[secondWord.charAt(i) - 'a'] < 0) {
46 return true;
47 }
48 }
49
50 // All character counts matched - words are anagrams
51 return false;
52 }
53}
54
1class Solution {
2public:
3 vector<string> removeAnagrams(vector<string>& words) {
4 // Lambda function to check if two strings are NOT anagrams
5 // Returns true if strings are different (not anagrams), false if they are anagrams
6 auto areNotAnagrams = [](const string& str1, const string& str2) -> bool {
7 // Different lengths mean they cannot be anagrams
8 if (str1.size() != str2.size()) {
9 return true;
10 }
11
12 // Count frequency of each character in the first string
13 int charFrequency[26] = {0};
14 for (const char& ch : str1) {
15 charFrequency[ch - 'a']++;
16 }
17
18 // Decrement frequency for each character in the second string
19 // If any frequency goes negative, strings are not anagrams
20 for (const char& ch : str2) {
21 charFrequency[ch - 'a']--;
22 if (charFrequency[ch - 'a'] < 0) {
23 return true;
24 }
25 }
26
27 // All frequencies match, strings are anagrams
28 return false;
29 };
30
31 // Result vector starts with the first word (it's never removed)
32 vector<string> result;
33 result.push_back(words[0]);
34
35 // Iterate through remaining words and keep only those that are not
36 // anagrams of their immediate predecessor
37 for (int i = 1; i < words.size(); i++) {
38 if (areNotAnagrams(words[i - 1], words[i])) {
39 result.push_back(words[i]);
40 }
41 }
42
43 return result;
44 }
45};
46
1/**
2 * Removes consecutive anagrams from an array of words
3 * @param words - Array of strings to process
4 * @returns Array with consecutive anagrams removed
5 */
6function removeAnagrams(words: string[]): string[] {
7 // Initialize result array with the first word
8 const result: string[] = [words[0]];
9
10 /**
11 * Checks if two strings are NOT anagrams
12 * @param firstWord - First string to compare
13 * @param secondWord - Second string to compare
14 * @returns true if strings are NOT anagrams, false if they are anagrams
15 */
16 const areNotAnagrams = (firstWord: string, secondWord: string): boolean => {
17 // Different lengths mean they cannot be anagrams
18 if (firstWord.length !== secondWord.length) {
19 return true;
20 }
21
22 // Create frequency array for 26 lowercase letters
23 const charFrequency: number[] = Array(26).fill(0);
24
25 // Count character frequencies in the first word
26 for (const char of firstWord) {
27 const index = char.charCodeAt(0) - 97; // Convert 'a'-'z' to 0-25
28 charFrequency[index]++;
29 }
30
31 // Subtract character frequencies from the second word
32 for (const char of secondWord) {
33 const index = char.charCodeAt(0) - 97; // Convert 'a'-'z' to 0-25
34 charFrequency[index]--;
35
36 // If frequency goes negative, words are not anagrams
37 if (charFrequency[index] < 0) {
38 return true;
39 }
40 }
41
42 // If all frequencies match, words are anagrams
43 return false;
44 };
45
46 // Iterate through remaining words
47 for (let i = 1; i < words.length; i++) {
48 // Only add word if it's not an anagram of the previous word
49 if (areNotAnagrams(words[i - 1], words[i])) {
50 result.push(words[i]);
51 }
52 }
53
54 return result;
55}
56
Time and Space Complexity
The time complexity is O(n Γ m)
, where n
is the number of words in the array and m
is the average length of each word. This is because:
- We iterate through all pairs of consecutive words using
pairwise(words)
, which takesO(n)
iterations - For each pair, the
check
function is called, which:- Compares lengths in
O(1)
- Creates a
Counter
from strings
inO(m)
time - Iterates through string
t
to decrement counts inO(m)
time
- Compares lengths in
- Therefore, the total time is
O(n Γ m)
Since the total length of all words combined is L
, and L = n Γ m
, the time complexity can also be expressed as O(L)
.
The space complexity is O(|Ξ£|)
where |Ξ£| = 26
for lowercase English letters. This is because:
- The
Counter
object in thecheck
function stores at most|Ξ£|
unique characters - The output list in the worst case contains all
n
words, but this is considered part of the output and not auxiliary space - The
pairwise
iterator and other variables useO(1)
additional space
Therefore, the auxiliary space complexity is O(26) = O(1)
or more precisely O(|Ξ£|)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Removal Process
The Mistake: Many developers initially think they need to repeatedly scan the entire array after each removal, implementing something like:
def removeAnagrams(self, words: List[str]) -> List[str]:
changed = True
while changed:
changed = False
i = 1
while i < len(words):
if are_anagrams(words[i-1], words[i]):
words.pop(i)
changed = True
else:
i += 1
return words
Why It's Wrong: This approach has several issues:
- Inefficiency: It has O(nΒ²) iterations in the worst case
- Unnecessary complexity: The problem guarantees a unique final result regardless of removal order
- Modifying the list while iterating can lead to bugs
The Fix: Recognize that you only need a single pass. Once you decide whether to keep a word based on its predecessor, that decision is final:
result = [words[0]]
for i in range(1, len(words)):
if not are_anagrams(words[i-1], words[i]):
result.append(words[i])
return result
Pitfall 2: Comparing Against the Wrong Previous Word
The Mistake: Some solutions incorrectly compare each word against the last word added to the result:
def removeAnagrams(self, words: List[str]) -> List[str]:
result = [words[0]]
for i in range(1, len(words)):
# WRONG: Comparing with result[-1] instead of words[i-1]
if not are_anagrams(result[-1], words[i]):
result.append(words[i])
return result
Why It's Wrong:
This changes the problem's semantics. Consider ["abc", "xyz", "bca"]
:
- Correct approach: Compare "xyz" with "abc" (not anagrams, keep "xyz"), then "bca" with "xyz" (not anagrams, keep "bca"). Result:
["abc", "xyz", "bca"]
- Wrong approach: Compare "bca" with "abc" (ARE anagrams, skip "bca"). Result:
["abc", "xyz"]
The Fix: Always compare consecutive words from the original array:
for prev_word, curr_word in pairwise(words): if is_not_anagram(prev_word, curr_word): result.append(curr_word)
Pitfall 3: Incorrect Anagram Checking
The Mistake: Using set comparison or simple character counting without considering frequency:
def are_anagrams(s, t):
# WRONG: This only checks if they have the same unique characters
return set(s) == set(t)
# Or
def are_anagrams(s, t):
# WRONG: This doesn't account for character frequency
return all(c in t for c in s) and all(c in s for c in t)
Why It's Wrong:
set("aab") == set("ab")
returnsTrue
, but "aab" and "ab" are not anagrams- The second approach would consider "aaa" and "a" as anagrams
The Fix: Use proper frequency counting:
def are_anagrams(s, t):
return Counter(s) == Counter(t)
# Or use sorted comparison
def are_anagrams(s, t):
return sorted(s) == sorted(t)
Pitfall 4: Edge Case Handling
The Mistake: Not handling single-element arrays or assuming the array has at least 2 elements:
def removeAnagrams(self, words: List[str]) -> List[str]:
# WRONG: Assumes words has at least 2 elements
result = [words[0]]
for s, t in pairwise(words): # This fails if len(words) == 1
# ...
Why It's Wrong:
When words = ["abc"]
, pairwise
returns an empty iterator, but the code still works correctly because we initialize with words[0]
. However, if you forget to initialize or handle empty arrays, it breaks.
The Fix:
The provided solution handles this correctly by always including words[0]
first. For extra safety, you could add:
if len(words) <= 1:
return words
# Continue with main logic
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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!