Find Common Characters
EasyPrompt
Given a string array words, return an array of all characters that show up in all strings within the words (including duplicates). You may return the answer in any order.
Example 1:
Input: words = ["bella","label","roller"]
Output: ["e","l","l"]Example 2:
Input: words = ["cool","lock","cook"]
Output: ["c","o"]
Constraints:
1 <= words.length <= 1001 <= words[i].length <= 100words[i]consists of lowercase English letters.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses the first word as a reference and iteratively finds the intersection of characters with each subsequent word. To correctly handle duplicate characters, it converts strings to lists of characters and removes characters from the lists as they are matched. This ensures that if a character appears k times in the common set, it must have appeared at least k times in every word.
Algorithm
- Initialize a list of characters,
commonChars, with the characters from the first word,words[0]. - Loop through the rest of the words in the
wordsarray, from the second word to the last. - For each word, create a temporary list of its characters,
currentWordChars. - Create a new empty list,
newCommonChars, to store the characters that are common up to this point. - Iterate through each character
cin thecommonCharslist. - Check if
cexists incurrentWordChars. - If it does, add
ctonewCommonCharsand remove one instance ofcfromcurrentWordCharsto ensure duplicates are handled correctly. - After checking all characters from
commonChars, updatecommonCharsto benewCommonChars. - After iterating through all the words, convert the final
commonCharslist of characters into a list of strings and return it.
Walkthrough
The algorithm begins by converting the first word into a list of characters, which serves as our initial set of potential common characters. It then iterates through the remaining words one by one. In each iteration, it compares the current set of common characters against the characters of the current word. A new list is built containing only the characters present in both. A crucial step is that whenever a character is matched and added to the new common list, it is removed from the current word's character list. This prevents the same character instance from being matched multiple times. This process of refining the common character list continues until all words have been processed. The final list contains all characters, including duplicates, that are present in every single word.
import java.util.ArrayList;import java.util.List; class Solution { public List<String> commonChars(String[] words) { if (words == null || words.length == 0) { return new ArrayList<>(); } List<Character> commonChars = new ArrayList<>(); for (char c : words[0].toCharArray()) { commonChars.add(c); } for (int i = 1; i < words.length; i++) { List<Character> currentWordChars = new ArrayList<>(); for (char c : words[i].toCharArray()) { currentWordChars.add(c); } List<Character> newCommonChars = new ArrayList<>(); for (Character c : commonChars) { if (currentWordChars.contains(c)) { newCommonChars.add(c); currentWordChars.remove(c); // Remove one occurrence to handle duplicates } } commonChars = newCommonChars; } List<String> result = new ArrayList<>(); for (Character c : commonChars) { result.add(String.valueOf(c)); } return result; }}Complexity
Time
O(N * M^2), where N is the number of words and M is the maximum length of a word. The outer loop runs N-1 times. Inside, we iterate through the common characters list (size up to M), and for each character, we perform `contains` and `remove` operations on another list, both of which take O(M) time. This results in an O(M^2) operation for each word.
Space
O(M), where M is the maximum length of a word. We use several lists (`commonChars`, `currentWordChars`, `newCommonChars`) whose sizes are at most M. The result list also takes O(M) space.
Trade-offs
Pros
The logic is relatively straightforward, following a direct simulation of finding an intersection.
It's easy to implement without requiring knowledge of more optimized data structures like frequency maps.
Cons
Highly inefficient due to repeated list traversals and removals. The
List.remove(Object)operation has a linear time complexity, leading to a quadratic time complexity relative to word length.Creates multiple intermediate lists, which can lead to higher memory churn and pressure on the garbage collector.
This is a highly efficient approach that uses frequency count arrays to solve the problem. Since the input consists only of lowercase English letters, we can use an array of size 26 to track the counts of each character. The core idea is to find the minimum frequency of each character across all words. This minimum frequency represents how many times a character can appear in the final result.
Algorithm
- Create an integer array
minFreqof size 26. Initialize it by counting the character frequencies of the first word,words[0]. - Iterate through the rest of the words in the array (from the second word onwards).
- For each word, create a temporary frequency array
currentFreqof size 26 and populate it with the character counts of that word. - After processing the current word, update the
minFreqarray. For each character from 'a' to 'z' (index 0 to 25), set its count inminFreqto the minimum of its current count and its count incurrentFreq. (minFreq[j] = Math.min(minFreq[j], currentFreq[j])). - After iterating through all words, the
minFreqarray holds the counts of characters common to all words. - Create a final result list. Iterate through the
minFreqarray from index 0 to 25. For each indexi, add the corresponding character to the result listminFreq[i]times. - Return the result list.
Walkthrough
The algorithm is optimized by avoiding costly string and list operations. It starts by creating a frequency array, minFreq, representing the character counts of the first word. This array acts as our initial baseline for common characters. Then, it iterates through the remaining words. For each word, it computes a new frequency array, currentFreq. The crucial step is updating minFreq by comparing it with currentFreq element by element and keeping the minimum value for each character's count. For instance, if minFreq has a count of 3 for 'l' and currentFreq has a count of 2, the new minFreq count for 'l' becomes 2. This ensures that minFreq always stores the bottleneck, i.e., the minimum number of times each character has appeared so far. After processing all words, minFreq contains the exact counts for the final common characters. The result list is then constructed based on these final counts.
import java.util.ArrayList;import java.util.List;import java.util.Arrays; class Solution { public List<String> commonChars(String[] words) { int[] minFreq = new int[26]; // Initialize with the frequencies from the first word for (char c : words[0].toCharArray()) { minFreq[c - 'a']++; } // Iterate through the rest of the words to find the minimum frequencies for (int i = 1; i < words.length; i++) { int[] currentFreq = new int[26]; for (char c : words[i].toCharArray()) { currentFreq[c - 'a']++; } // Update the global minimum frequency for each character for (int j = 0; j < 26; j++) { minFreq[j] = Math.min(minFreq[j], currentFreq[j]); } } // Build the result list from the final minimum frequencies List<String> result = new ArrayList<>(); for (int i = 0; i < 26; i++) { while (minFreq[i] > 0) { result.add(String.valueOf((char) ('a' + i))); minFreq[i]--; } } return result; }}Complexity
Time
O(L), where L is the total number of characters in all words combined (sum of all word lengths). We iterate through each character of each word exactly once to build frequency maps. The update step takes a constant O(26) time for each of the N words. Thus, the total time is proportional to the total input size.
Space
O(1). The space used for the frequency arrays (`minFreq`, `currentFreq`) is constant (26 integers), regardless of the input size. The space for the output list is not typically counted in complexity analysis, but it would be O(M) where M is the length of the shortest word.
Trade-offs
Pros
Extremely efficient in both time and space.
Scales linearly with the total number of characters, making it suitable for large inputs.
Uses fixed-size arrays, which are very fast and memory-efficient.
Cons
The logic might be slightly less intuitive at first glance for beginners compared to direct character comparison.
Solutions
Solution
class Solution {public List<String> commonChars(String[] words) { int[] cnt = new int[26]; Arrays.fill(cnt, 10000); for (String w : words) { int[] ccnt = new int[26]; for (int i = 0; i < w.length(); ++i) { ++ccnt[w.charAt(i) - 'a']; } for (int i = 0; i < 26; ++i) { cnt[i] = Math.min(cnt[i], ccnt[i]); } } List<String> ans = new ArrayList<>(); for (int i = 0; i < 26; ++i) { while (cnt[i]-- > 0) { ans.add(String.valueOf((char)(i + 'a'))); } } return ans; }}class Solution {public: vector<string> commonChars(vector<string> &words) { int cnt[26]; memset(cnt, 0x3f, sizeof(cnt)); for (auto &w : words) { int ccnt[26]{}; for (char &c : w) { ++ccnt[c - 'a']; } for (int i = 0; i < 26; ++i) { cnt[i] = min(cnt[i], ccnt[i]); } } vector<string> ans; for (int i = 0; i < 26; ++i) { while (cnt[i]--) { ans.emplace_back(1, i + 'a'); } } return ans; }};class Solution: def commonChars(self, words: List[str]) -> List[str]: cnt = Counter(words[0]) for w in words: ccnt = Counter(w) for c in cnt . keys(): cnt[c] = min(cnt[c], ccnt[c]) ans = [] for c, v in cnt . items(): ans . extend([c] * v) return ansVideo 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.