Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°27

Top-K Frequent Elements

Medium Hash MapHeapsInterview Classic

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.

SOLVE IT YOURSELF

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.

YOUR TASK

Implement top_k_frequent(nums, k) → the k most frequent values, frequency descending, ties broken by smaller value.

CPython · WebAssembly
Examples and constraints 3 examples
EXAMPLE 1
Input nums = [1,1,1,2,2,3], k = 2
Output [1, 2]
1 appears 3×, 2 appears 2×
EXAMPLE 2
Input nums = [4,4,5,5,6], k = 2
Output [4, 5]
tie at 2× → smaller value first
EXAMPLE 3
Input nums = [7], k = 1
Output [7]
trivial but a real edge case
CONSTRAINTS
  • 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
HINTS — 4 IDEAS
  1. Count with a dict / Map first — one pass.
  2. Then sort the distinct values by (-count, value) and take the first k.
  3. The tie-break is exactly what the tuple sort key encodes.
  4. 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.

stepactionstate
1x = 1counts = {1: 1}
2x = 1counts = {1: 2}
3x = 1counts = {1: 3}
4x = 2counts = {1: 3, 2: 1}
5x = 2counts = {1: 3, 2: 2}
6x = 3counts = {1: 3, 2: 2, 3: 1}
sortkey (-counts[v], v)keys (-3, 1), (-2, 2), (-1, 3), so ranked = [1, 2, 3]
sliceranked[: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=True on (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.nsmallest or 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, replace a - b with a string comparison, either a.localeCompare(b) or a plain a < b test. 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].

▶  Watch it explained
Finished this one? 0 / 76 Challenges done
NextNumber of Islands Next on the “Interview classics” ladder · rung 8 of 10

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges