Skip to content
← All posts

Find Common Characters: Frequency Intersection

5 min read
leetcodeproblemeasyarraysstringshash-map

Find Common Characters (LeetCode 1002) asks you to find every character that appears in all strings within a given list, including duplicates. It is a clean application of the frequency counting pattern, and once you see how the element-wise minimum works, the solution writes itself.

The problem

Given a string array words, return an array of all characters that show up in all strings within the list (including duplicates). You may return the answer in any order.

Example: words = ["bella", "label", "roller"]

Output: ["e", "l", "l"]

The character "e" appears at least once in every word. The character "l" appears at least twice in every word. So both "l" instances make it into the result.

abelor"bella"a1b1e1l2o0r0"label"a1b1e1l2o0r0"roller"a0b0e1l2o1r2minresulta0b0e1l2o0r0common chars: ["e", "l", "l"]
Count character frequencies for each word, then take the element-wise minimum. Non-zero results are the common characters.

The question boils down to: for each character, what is the minimum number of times it appears across all words?

The approach

The idea is to count character frequencies for each word, then compute the element-wise minimum across all those frequency arrays. If a character appears 3 times in one word but only 1 time in another, the common count is 1. If a character is missing from any word, its minimum is 0 and it does not appear in the result.

Here is the plan:

  1. Count the frequency of each character in the first word. This is your initial result.
  2. For each subsequent word, count its character frequencies and update the result by taking the minimum for each character.
  3. After processing all words, expand the result frequencies into a list of characters.

One pass through each word. One frequency array that shrinks as you go. That is it.

The code

from collections import Counter

def common_chars(words: list[str]) -> list[str]:
    result = Counter(words[0])
    for word in words[1:]:
        result &= Counter(word)
    return list(result.elements())

Let's break down what each line does:

  • result = Counter(words[0]) initializes the running frequency count from the first word. For "bella", this gives {'b': 1, 'e': 1, 'l': 2, 'a': 1}.
  • result &= Counter(word) is the intersection operator on Counters. For each key, it takes the minimum of the two counts. This is exactly the element-wise minimum we need.
  • result.elements() expands the counter back into individual characters. If 'l' has count 2, it produces 'l', 'l'.

Python's Counter has a built-in intersection operator (&), which makes this problem almost trivial once you know about it.

Alternative: manual frequency arrays

If you want to avoid Counter (or your interviewer asks you to implement it from scratch), you can use a fixed-size array of 26 integers:

def common_chars(words: list[str]) -> list[str]:
    result = [float('inf')] * 26
    for word in words:
        count = [0] * 26
        for ch in word:
            count[ord(ch) - ord('a')] += 1
        for i in range(26):
            result[i] = min(result[i], count[i])

    output = []
    for i in range(26):
        output.extend([chr(i + ord('a'))] * result[i])
    return output

This version initializes every slot to infinity, so the first word's counts naturally take over via min(). Each subsequent word can only shrink the counts, never grow them. At the end, you expand the array back into characters.

Both approaches are O(n * k) where n is the number of words and k is the average word length. The manual version avoids the overhead of Counter objects and is slightly faster in practice, though either is perfectly fine for an interview.

Step-by-step walkthrough

Let's trace through words = ["bella", "label", "roller"] one word at a time, watching the result frequencies evolve:

Step 1: Initialize result frequencies from "bella"

abelor"bella"a1b1e1l2o0r0copyresulta1b1e1l2o0r0

Start by counting characters in the first word. This is our initial set of "common" frequencies.

Step 2: Intersect with "label" using element-wise minimum

abelorresulta1b1e1l2o0r0"label"a1b1e1l2o0r0min()resulta1b1e1l2o0r0

For each character, take min(result, "label" count). "b" drops from 1 to 1 (same), "l" stays at 2, "a" stays at 1, but "o" and "r" stay at 0.

Step 3: Intersect with "roller" using element-wise minimum

abelorresulta1b1e1l2o0r0"roller"a0b0e1l2o1r2min()resulta0b0e1l2o0r0Non-zero counts: e=1, l=2Output: ["e", "l", "l"]

Final pass. "a" drops to 0 (not in "roller"), "b" drops to 0, "e" stays at 1, "l" stays at min(2, 2) = 2. The final common characters are ["e", "l", "l"].

After processing all three words, the non-zero entries in the result are the common characters. Expand each entry by its count to get the final answer.

Complexity analysis

ApproachTimeSpace
Counter intersectionO(n * k)O(1)*
Manual frequency arrayO(n * k)O(1)

*O(1) because the character set is bounded at 26 lowercase English letters. The frequency arrays never grow beyond 26 entries regardless of input size.

n is the number of words and k is the average word length. You do one pass through each word to count characters, and one comparison pass per word to update the result. Both are linear in the total number of characters across all words.

The building blocks

This problem is built from two core building blocks that appear in many other problems:

Frequency counting

The act of walking through a collection and counting how many times each element appears. The template is:

count = [0] * 26
for ch in word:
    count[ord(ch) - ord('a')] += 1

You have already seen this in Valid Anagram (count and compare), Group Anagrams (count as a hash key), and Find All Anagrams in a String (count inside a sliding window). The building block is always the same. What changes is what you do with the counts afterward.

Element-wise aggregation

The act of combining two arrays position by position using some operation. Here the operation is min(), but you will see max(), sum(), and product() in other problems. The pattern is:

for i in range(len(a)):
    result[i] = op(result[i], b[i])

This "reduce across multiple arrays" pattern shows up whenever you need to combine information from multiple sources into a single summary. In this problem, each word contributes a frequency array, and you reduce them with min() to find the intersection.

Edge cases to watch for

A few things to keep in mind:

  • Single word. If words has only one element, every character in that word is "common." The algorithm handles this correctly because the result starts as the first word's frequencies and there are no subsequent words to intersect with.
  • No common characters. If some character is missing from any word, min() drives its count to 0. The output is an empty list. No special handling needed.
  • All identical words. If every word is "aaa", the result is ["a", "a", "a"]. The intersection of identical frequency arrays is just that same array. Works naturally.
  • Words of different lengths. The algorithm does not care about word lengths. It only cares about per-character frequencies within each word. A 3-letter word and a 10-letter word are handled identically.

From understanding to recall

Find Common Characters is one of those problems that feels obvious once you see the solution. "Just count characters and take the minimum." But the building block here is broader than this single problem. The frequency intersection pattern appears whenever you need to find the overlap between multiple collections while respecting multiplicity.

Three weeks from now, when you encounter a problem that asks "which elements appear in all of these lists, including duplicates," you want the Counter &= pattern (or the manual min-reduce over frequency arrays) to fire automatically. Not something you re-derive from scratch.

Spaced repetition makes this stick. You practice typing the frequency intersection template from memory at increasing intervals. After a few cycles, the pattern is automatic. You see "common to all" and your fingers already know what to type.

Related posts

  • Valid Anagram - The single-pair version of frequency counting, where you compare two strings instead of intersecting across many
  • Group Anagrams - Uses frequency counting to generate canonical keys for grouping, the same counting technique applied differently
  • Intersection of Two Arrays - The set-based version of this problem, where duplicates are removed instead of preserved