37. Top-K Frequent Intents
Problem
A reporting job scans a stream of intent labels, one per handled conversation (e.g.
"billing", "refund", "account"), and needs the k most common intents to
surface on a dashboard. Given the full list of intent labels and k, return
the k intents that occur most often, most frequent first, without fully
sorting every distinct intent by frequency.
Ties in frequency are broken alphabetically ascending. The point of the problem is to
avoid an O(m log m) sort over all m distinct intents when you only need the
top k of them.
Source: src/37_top_k_frequent_intents.py
def top_k_frequent(intents: list[str], k: int) -> list[str]:
>>> top_k_frequent(["billing","billing","refund","account","refund","billing"], k=2)
['billing', 'refund']
>>> top_k_frequent(["b","a","a","b","c"], k=2)
['a', 'b']
Step-by-Step Approach
- Count occurrences of every intent label in a single O(n) pass, using a hash map (e.g.
collections.Counter). - Recognize that once you have counts, the remaining question is "select the top k by count" over the
mdistinct keys — a classic top-k selection problem, not a full sort. - Use a heap-based selection (
heapq.nlargest, or an equivalent manual min-heap of sizek) over the distinct intents rather than sorting all of them. - Define a sort key that puts higher frequency first, and among equal frequencies, breaks ties alphabetically ascending — for example
(count, tuple(-ord(ch) for ch in word)), so that within a tie, "a" and "b" order lexicographically rather than by insertion order. - Handle
k = 0(return an empty list) andklarger than the number of distinct intents (return every distinct intent, still ordered by frequency then alphabetically) as edge cases. - Return the selected intents as a list, most frequent first.
The key insight is that counting is unavoidable at O(n), but selecting the top k
of m distinct items only needs O(m log k) via a heap, which beats an O(m log m)
full sort whenever k is much smaller than the number of distinct items.
Reference solution
import heapq
from collections import Counter
def top_k_frequent(intents: list[str], k: int) -> list[str]:
# count once O(n), then heapq.nlargest over the m distinct intents is O(m log k);
# key negates each char's ord so alphabetically-earlier words win frequency ties
# distinct intent -> count, one O(n) pass
counts = Counter(intents)
# tuple key: count first, then negated ord() to break ties alphabetically
return heapq.nlargest(k, counts.keys(), key=lambda w: (counts[w], tuple(-ord(ch) for ch in w)))
Key Functions & Tricks
collections.Counter(intents)— counts each distinct item in one O(n) pass.heapq.nlargest(k, iterable, key=None)— top-k via bounded heap, O(m log k) not O(m log m).counts.keys()— distinct intents, the candidate pool passed tonlargest.key=lambda w: (counts[w], ...)— tuple key compares lexicographically, count first then tie-break.ord(ch)— character's Unicode code point, used to build a comparable tie-break value.tuple(-ord(ch) for ch in w)— negated code points turn "alphabetically first" into "numerically largest".
How to Recognize This Pattern
Signals: the phrase "top k" or "k most frequent" paired with a request to avoid sorting
everything, or an explicit hint that the number of distinct items can be large while
k stays small. Any "frequency count, then select the top few" shape is a
heap problem: count with a hash map, then use a bounded-size heap (or a language
built-in like heapq.nlargest) to pick the top k.
Common variations: "top k frequent elements" over arbitrary hashable items instead of
strings; "k closest points to origin" (min-heap by distance instead of by count);
or streaming variants where you maintain a running top-k heap of size k
as new items arrive, rather than counting everything up front.
Common pitfall: forgetting the tie-breaking rule. If two intents have equal frequency, an interviewer-stated tie-break (like "alphabetically ascending") must be baked into the sort/heap key itself, not left to whatever order the hash map happens to iterate in — Python dict iteration order is insertion order, not alphabetical, so it's easy to get an accidentally-correct-looking answer on small examples that breaks on a real test case.