Find Resultant Array After Removing Anagrams
EasyPrompt
You are given a 0-indexed string array words, where words[i] consists of lowercase English letters.
In one operation, select any index i such that 0 < i < words.length and words[i - 1] and words[i] are anagrams, and delete words[i] from words. Keep performing this operation as long as you can select an index that satisfies the conditions.
Return words after performing all operations. It can be shown that selecting the indices for each operation in any arbitrary order will lead to the same result.
An Anagram is a word or phrase formed by rearranging the letters of a different word or phrase using all the original letters exactly once. For example, "dacb" is an anagram of "abdc".
Example 1:
Input: words = ["abba","baba","bbaa","cd","cd"]
Output: ["abba","cd"]
Explanation:
One of the ways we can obtain the resultant array is by using the following operations:
- Since words[2] = "bbaa" and words[1] = "baba" are anagrams, we choose index 2 and delete words[2].
Now words = ["abba","baba","cd","cd"].
- Since words[1] = "baba" and words[0] = "abba" are anagrams, we choose index 1 and delete words[1].
Now words = ["abba","cd","cd"].
- Since words[2] = "cd" and words[1] = "cd" are anagrams, we choose index 2 and delete words[2].
Now words = ["abba","cd"].
We can no longer perform any operations, so ["abba","cd"] is the final answer.Example 2:
Input: words = ["a","b","c","d","e"]
Output: ["a","b","c","d","e"]
Explanation:
No two adjacent strings in words are anagrams of each other, so no operations are performed.
Constraints:
1 <= words.length <= 1001 <= words[i].length <= 10words[i]consists of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach iterates through the input array words a single time. It determines whether to keep a word by comparing it to the last word that was kept. To simplify the anagram check, it converts each word into a "canonical form". For this approach, the canonical form is the string created by sorting the word's characters alphabetically. If the current word's canonical form is different from the last kept word's canonical form, the current word is added to the result.
Algorithm
- Initialize an empty
ArrayListcalledresult. - If the input
wordsarray is empty, return the empty list. - Create a placeholder string
lastCanonicalinitialized to an empty string or any value that won't match a real canonical string. - Iterate through each
wordin the inputwordsarray. - For each
word, compute its "canonical form". A canonical form is a representation that is identical for all anagrams of a word. Here, we use the sorted version of the string as its canonical form. - To get the canonical form, convert the
wordto a character array, sort the array, and convert it back to a string. - Compare the
currentCanonicalform with thelastCanonicalform. - If they are not equal, it means the current
wordis not an anagram of the previously kept word. In this case, add thewordto theresultlist and updatelastCanonicalto thecurrentCanonicalform. - If they are equal, do nothing, effectively skipping the current word.
- After iterating through all the words, return the
resultlist.
Walkthrough
The core idea is that two words are anagrams if and only if their sorted character sequences are identical. For example, "baba" and "abba" both become "aabb" when sorted. We can leverage this property.
We process the words array from left to right. We keep a variable, lastCanonical, to store the sorted form of the last word we added to our result list. For each new word we encounter, we compute its sorted form, currentCanonical. If currentCanonical is the same as lastCanonical, we know it's an anagram of the previous word and we discard it. If it's different, we add the new word to our result list and update lastCanonical with currentCanonical.
import java.util.ArrayList;import java.util.Arrays;import java.util.List; class Solution { private String getCanonical(String s) { char[] chars = s.toCharArray(); Arrays.sort(chars); return new String(chars); } public List<String> removeAnagrams(String[] words) { List<String> result = new ArrayList<>(); String lastCanonical = ""; // An initial value that won't match for (String word : words) { String currentCanonical = getCanonical(word); if (!currentCanonical.equals(lastCanonical)) { result.add(word); lastCanonical = currentCanonical; } } return result; }}Complexity
Time
O(N * K log K), where `N` is the number of words and `K` is the maximum length of a word. We iterate through `N` words. For each word, we compute its canonical form by sorting its characters, which takes `O(K log K)` time.
Space
O(N * K), where `N` is the number of words and `K` is the maximum length of a word. This space is primarily used for the `result` list. Additionally, `O(K)` space is used temporarily for the character arrays and canonical strings within the loop.
Trade-offs
Pros
The logic is straightforward and easy to implement using built-in sorting functions.
It solves the problem in a single pass, which is much more efficient than repeated deletions on a list.
Cons
The sorting step (
O(K log K)) for each word makes this approach less efficient than using frequency counts, especially for longer words.
This approach is an optimization of the previous one. It also performs a single pass over the input array but uses a more efficient method for the anagram check. Instead of sorting strings, it calculates the character frequency count for each word (e.g., using an array of 26 integers for lowercase English letters). A word is kept if its frequency count differs from the last kept word's frequency count. This avoids the O(K log K) sorting overhead, leading to a better time complexity.
Algorithm
- Initialize an empty
ArrayListcalledresult. - If the input
wordsarray is empty, return the empty list. - Initialize a
lastFrequencyinteger array of size 26 to a non-matching initial state (e.g., an empty array or an array of -1s). - Iterate through each
wordin the inputwordsarray. - For each
word, compute its character frequency array. This is an integer array of size 26 where the i-th element stores the count of the i-th letter of the alphabet. - Compare the
currentFrequencyarray with thelastFrequencyarray usingArrays.equals(). - If the arrays are not equal, it signifies that the current
wordis not an anagram of the previously kept word. Add thewordto theresultlist and updatelastFrequencyto be thecurrentFrequencyarray. - If the arrays are equal, do nothing.
- After the loop finishes, return the
resultlist.
Walkthrough
Two strings are anagrams if they have the exact same character counts. We can represent the character counts of a string using an integer array of size 26. For example, the word "baba" would be represented by an array where the count for 'a' is 2, the count for 'b' is 2, and all other counts are 0.
This method iterates through the words, calculating the frequency array for each. It compares the current word's frequency array with the frequency array of the last word added to the result. If the arrays are different, the word is kept. This is more efficient because calculating the frequency array takes linear time (O(K)) with respect to the word's length, which is better than the O(K log K) time required for sorting.
import java.util.ArrayList;import java.util.Arrays;import java.util.List; class Solution { private int[] getFrequency(String s) { int[] counts = new int[26]; for (char c : s.toCharArray()) { counts[c - 'a']++; } return counts; } public List<String> removeAnagrams(String[] words) { List<String> result = new ArrayList<>(); int[] lastFrequency = new int[0]; // Initial state that won't match for (String word : words) { int[] currentFrequency = getFrequency(word); if (!Arrays.equals(currentFrequency, lastFrequency)) { result.add(word); lastFrequency = currentFrequency; } } return result; }}Complexity
Time
O(N * K), where `N` is the number of words and `K` is the maximum length of a word. We iterate through `N` words. For each word, computing its frequency array takes `O(K)` time. Comparing two frequency arrays takes `O(1)` time as the alphabet size is constant.
Space
O(N * K), where `N` is the number of words and `K` is the maximum length of a word. This is for storing the `result` list. The space for the frequency arrays is `O(1)` because their size (26) is constant and does not depend on the input size.
Trade-offs
Pros
Achieves the optimal time complexity for this problem.
More efficient than the sorting approach, especially as the length of words (
K) increases.
Cons
The logic for creating and comparing frequency arrays is slightly more complex to write than simply sorting and comparing strings.
Solutions
Solution
class Solution {public List<String> removeAnagrams(String[] words) { List<String> ans = new ArrayList<>(); String prev = ""; for (String w : words) { char[] cs = w.toCharArray(); Arrays.sort(cs); String t = String.valueOf(cs); if (!t.equals(prev)) { ans.add(w); } prev = t; } return ans; }}class Solution {public: vector<string> removeAnagrams(vector<string> &words) { auto check = [](string &s, string &t) -> bool { if (s.size() != t.size()) { return true; } int cnt[26]{}; for (char &c : s) { ++cnt[c - 'a']; } for (char &c : t) { if (--cnt[c - 'a'] < 0) { return true; } } return false; }; vector<string> ans = {words[0]}; for (int i = 1; i < words.size(); ++i) { if (check(words[i - 1], words[i])) { ans.emplace_back(words[i]); } } return ans; }};class Solution: def removeAnagrams(self, words: List[str]) -> List[str]: return [ w for i, w in enumerate(words) if i == 0 or sorted(w) != sorted(words[i - 1])]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.