Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Top-K Frequent Elements
Count, then rank — the two-step pattern behind trending topics, hot keys, and a thousand interview questions. A hash map does the counting; the ranking is where heaps, buckets, or a simple sort earn their keep.
The problem
Given nums, a list of integers, and k, return the k most frequent values, ordered by frequency descending; break frequency ties by the smaller value first. You may assume k ≤ the number of distinct values.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement top_k_frequent(nums, k) → the k most frequent values, frequency descending, ties broken by smaller value.
Examples and constraints 3 examples
nums = [1,1,1,2,2,3], k = 2[1, 2]nums = [4,4,5,5,6], k = 2[4, 5]nums = [7], k = 1[7]- Order: frequency descending, then value ascending on ties.
- k ≤ number of distinct values in nums.
- Aim for O(n log n) or better (bucket sort by count gets O(n)).
Hints 4 ideas
- Count with a dict / Map first — one pass.
- Then sort the distinct values by (-count, value) and take the first k.
- The tie-break is exactly what the tuple sort key encodes.
- For the O(n) flex: bucket values by count (counts are ≤ n), then walk buckets high to low.
Approach, complexity & discussion — open after you solve
Start from brute force
The obvious approach counts each distinct value by scanning the whole list: sorted(set(nums), key=lambda v: (-nums.count(v), v))[:k]. It works because nums.count(v) is exactly the frequency of v and the key encodes the required order. But every count is a full pass, so it costs O(n · d) time for n numbers with d distinct values, which is O(n²) when most values are distinct, plus O(d) space for the set. With 100,000 numbers, all distinct, that is ten billion element comparisons, where a single counting pass touches each number once.
The approach
Counting once turns n numbers into d (value, count) pairs, and the ranking needs nothing else. The reference builds counts in one pass with counts[x] = counts.get(x, 0) + 1, so afterwards counts[v] is exactly how often v occurs. One sort of the distinct values with the key (-counts[v], v) then orders them by frequency descending and, on equal frequency, by smaller value, which is precisely the output order, so ranked[:k] is the answer. Negating the count lets a single ascending sort run the two keys in opposite directions; the TypeScript version gets the same order from the comparator (counts.get(b)! - counts.get(a)!) || a - b.
The sort is over the d distinct values, not the n inputs, which is why it beats sorting the list. When d is large and k is small, the same key drives a size-k selection instead: heapq.nsmallest(k, counts, key=lambda v: (-counts[v], v)) returns the same answer in O(d log k).
Walk through an example
The first example, nums = [1, 1, 1, 2, 2, 3] and k = 2: one counting pass, one sort, one slice.
| step | action | state |
|---|---|---|
| 1 | x = 1 | counts = {1: 1} |
| 2 | x = 1 | counts = {1: 2} |
| 3 | x = 1 | counts = {1: 3} |
| 4 | x = 2 | counts = {1: 3, 2: 1} |
| 5 | x = 2 | counts = {1: 3, 2: 2} |
| 6 | x = 3 | counts = {1: 3, 2: 2, 3: 1} |
| sort | key (-counts[v], v) | keys (-3, 1), (-2, 2), (-1, 3), so ranked = [1, 2, 3] |
| slice | ranked[:2] | [1, 2] |
Complexity
Count, then sort: O(n + d log d) time for n numbers with d distinct values (one counting pass, one sort of the distinct values) and O(d) space for counts and ranked. A size-k heap makes the ranking O(d log k). Brute force: O(n · d) time, because each of the d counts rescans the whole list, and O(d) space.
Edge cases to test
- Ties that insertion order gets wrong:
[5, 5, 4, 4, 6], k = 2 must give[4, 5]. A key on count alone returns[5, 4], because the dict keeps first-appearance order and the sort is stable. - All values distinct:
[9, 8, 7], k = 2 gives[7, 8]. Every count is 1, so the tie-break alone decides. - k equal to the number of distinct values:
[3, 1, 2, 2], k = 3 gives[2, 1, 3], every value still in rank order, with 1 before 3 on their tie. - Negative values:
[-1, -1, 0], k = 1 gives[-1]. A counting array indexed by value breaks here, and silently in Python, where index -1 is the last slot. - A single element:
[7], k = 1 gives[7]. A bucket array needs n + 1 slots, because a count can equal n.
Common mistakes
- Recounting with
nums.count(v)inside the sort key: correct, but every call rescans the list, so it is O(n · d). - Breaking ties by accident: a key on count alone returns
[5, 4]for[5, 5, 4, 4, 6], k = 2, because Python’s sort is stable and the dict keeps first-appearance order. - Using
reverse=Trueon(count, value)to get counts descending: it flips the tie-break too, so ties come out larger value first. - Sizing a bucket array as n instead of n + 1: a value can occur n times, so
buckets[n]must exist.
Follow-ups an interviewer asks next
- The input is a stream too large to store, with too many distinct values to count.
- Exact top-k needs a counter per distinct value, so memory grows with d. When that is too much, approximate. Misra-Gries and Space-Saving keep a fixed number of counters, guarantee that every value above a frequency threshold of roughly n divided by the counter count survives, and bound the error of each count. A Count-Min sketch plus a size-k heap of candidates is the other common design.
- Can you beat sorting the distinct values?
- Yes. A size-k selection with the same key,
heapq.nsmallestor a min-heap that pops whenever it holds more than k, costs O(d log k). Buckets indexed by count, which runs from 1 to n, read from the high end, cost O(n) when ties need no order; with this problem’s tie rule, each bucket you read must also be ordered, which can cost O(d log d) when most values share one count. - Return the top k words instead of numbers, ties in alphabetical order.
- Same algorithm, different key: count the words, then sort by
(-count, word), since Python compares strings lexicographically. In TypeScript, replacea - bwith a string comparison, eithera.localeCompare(b)or a plaina < btest. Those two disagree on mixed case and non-ASCII text, so choose one and test it. - The counts keep changing, like a live trending list.
- Keep counts in a hash map and the ranking in an ordered structure keyed by
(-count, value), such as a balanced tree or a sorted container. Each increment removes the old key and inserts the new one in O(log d), and the top k are the first k entries. For trends that should fade, count per time bucket and drop buckets as they expire.
Where this shows up
“Most frequent” is everywhere: trending topics, top search queries, hot cache keys, word clouds, analytics dashboards. The keep-only-the-best-k-seen pattern (a bounded min-heap) generalizes to any top-k-from-a-stream problem where you cannot hold or sort everything — the same idea behind streaming heavy-hitter algorithms.
Reference solution
Reference solution — Python
def top_k_frequent(nums, k):
counts = {}
for x in nums:
counts[x] = counts.get(x, 0) + 1
ranked = sorted(counts, key=lambda v: (-counts[v], v))
return ranked[:k]
print(top_k_frequent([1, 1, 1, 2, 2, 3], 2)) # -> [1, 2]Reference solution — TypeScript
function topKFrequent(nums: number[], k: number): number[] {
const counts = new Map<number, number>();
for (const x of nums) counts.set(x, (counts.get(x) ?? 0) + 1);
return [...counts.keys()]
.sort((a, b) => (counts.get(b)! - counts.get(a)!) || a - b)
.slice(0, k);
}
console.log(topKFrequent([1, 1, 1, 2, 2, 3], 2)); // -> [1, 2]Questions about Top-K Frequent Elements
What is the fastest way to find the k most frequent elements?
Count every value once with a hash map, which is linear in the input. For the ranking, bucket the values by count: counts run from 1 to n, so an array of n + 1 lists holds them, and reading it from the high end yields values in frequency order in linear time overall. A size-k heap costs O(d log k) for d distinct values and is simpler when k is small. With a tie rule like this problem, smaller value first, each bucket you read must also be ordered, so sorting the d distinct values, as the reference does, is a reasonable default.
Why sort by negative count instead of reversing the sort?
Because the two keys need opposite directions. Frequency must go from high to low, but tied values must go from low to high. Reversing the whole sort flips both and puts the larger value first on ties, so [4, 4, 5, 5, 6] would rank 5 ahead of 4. Negating the count makes one ascending sort on the pair of negative count and value produce exactly the required order. In TypeScript the comparator does the same job: subtract the counts in reverse order, and when the difference is zero, fall back to a minus b.
Is a heap or a bucket sort better for top k frequent elements?
It depends on k and on the tie rule. A bucket sort is linear in n and suits a full ranking or a large k, but it allocates n + 1 buckets and needs extra work to order ties inside a bucket. A size-k heap runs in O(d log k), needs only O(k) extra space beyond the counts, and can consume counts as they arrive without knowing n in advance. For small k the heap is the usual practical choice; for k close to d, a plain sort of the distinct values is as good as either.
Does the order of equal-frequency elements matter?
Here it does: ties must list the smaller value first, and the tests check it, so [4, 4, 5, 5, 6] with k = 2 must return [4, 5]. Many versions of this problem accept any order, which is why solutions found online often skip the tie-break. Insertion order is not a tie-break: Python dictionaries keep first-appearance order and its sort is stable, so a key on count alone returns [5, 4] for [5, 5, 4, 4, 6].
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- Number of IslandsThe canonical connected-components question. Flood-fill each unvisited patch of land with BFS or DFS, count how many floods you started — grid traversal, visited bookkeeping, and the classic diagonal trap. Solve it in Python or TypeScript.Read →
- Merge IntervalsCalendar apps and memory allocators run on this: sort by start, sweep once, grow or close the current block. Touching endpoints merge, nested intervals vanish — the classic that punishes off-by-one thinking. Solve it in Python or TypeScript.Read →
- 3SumThe rite of passage: every unique zero-sum triplet, no duplicates, no O(n³). Sort once, fix one element, squeeze the rest with two pointers, and skip duplicates at all three levels. Solve it in Python or TypeScript.Read →