Top K Frequent Elements
Given an integer array nums and an integer k, return the k elements that appear most often.
Return the element values themselves, not their counts. The output may be in any order. The input guarantees the answer is unique — there is never a tie straddling the kth position that would make two different answers equally valid.
- 1 <= nums.length <= 10⁵
- -10⁴ <= nums[i] <= 10⁴
- k is in the range [1, number of distinct values in nums]
- The answer is guaranteed to be unique.
- Follow-up: your algorithm must run in better than O(N log N) time.
nums = [1,1,1,2,2,3], k = 2[1,2]nums = [4,1,-1,2,-1,2,3], k = 2[-1,2]nums = [7,7,8,9], k = 3[7,8,9]nums = [5], k = 1[5]Two separate questions live inside this one statement, and separating them is most of the work. First: how often does each value occur? Second: given those counts, which k values have the highest ones? The first question has one sensible answer. The second is the interesting part, and it is a problem we have already solved once.
To know that 1 appears three times you must visit every element, so no counting method beats one full pass. The only decision is where the running tallies live. A hash map stores key-to-value pairs and finds any key in roughly constant time — it converts the key into an array position by running it through a hash function, so a lookup is arithmetic followed by one memory access, not a search. That gives us the counts in O(N):
from collections import Counter
counts = Counter(nums) # {1: 3, 2: 2, 3: 1}Call the number of distinct values M. Note M can be much smaller than N — that is the whole point of counting, it shrinks the problem — but in the worst case, all elements distinct, M equals N.
def top_k_frequent(nums, k):
counts = Counter(nums)
ordered = sorted(counts, key=counts.get, reverse=True) # O(M log M)
return ordered[:k]Correct, short, and it is what most people write first. The cost is O(N + M log M), which is O(N log N) when values are mostly distinct. And the waste is precisely the one named in Kth Largest: sorting decides the relative rank of every pair of distinct values, including the ranks of everything below the cutoff, when the question only cares which k are on top and does not even care about the order among those. The follow-up explicitly asks for better than O(N log N), so this approach is what the interviewer wants you to improve on.
Everything from Kth Largest transfers directly. Walk the (value, count) pairs, keep a shortlist of the k with the highest counts in a min-heap, and evict the weakest whenever the shortlist overflows. Filling the three slots of that pattern:
import heapq
from collections import Counter
def top_k_frequent(nums, k):
counts = Counter(nums)
shortlist = []
for value, count in counts.items():
heapq.heappush(shortlist, (count, value)) # ordered by count first
if len(shortlist) > k:
heapq.heappop(shortlist) # drop the least frequent
return [value for count, value in shortlist]The heap holds at most k entries and each of the M pairs causes at most one push and one pop, so this is O(N + M log k) time and O(M) space — the map dominates the memory. Better than sorting whenever k is meaningfully smaller than M, which is the case the problem is built around.
O(N + M log k) still has a log in it. Getting rid of it needs an observation about the counts themselves, not the values. A count is a number of occurrences, so it is a whole number between 1 and N. There are only N possible counts, and they are small integers with no gaps in their range.
Whenever ranking keys are small bounded integers, you can stop comparing and start indexing. Build an array of N+1 empty lists, and place each value into the slot matching its count: everything appearing exactly 3 times goes into slot 3. Now the ranking is not computed at all — it is the geometry of the array. Sweep from the highest slot downward, collecting values until you have k of them.
def top_k_frequent(nums, k):
counts = Counter(nums)
buckets = [[] for _ in range(len(nums) + 1)] # index = frequency
for value, count in counts.items():
buckets[count].append(value)
result = []
for count in range(len(nums), 0, -1): # highest frequency first
for value in buckets[count]:
result.append(value)
if len(result) == k:
return result
return resultEvery phase is linear: counting is O(N), filling buckets is O(M), and the downward sweep touches N+1 slots and at most M values. Total O(N) time and O(N) space. This is the answer the follow-up is fishing for.
The trade is real, though, and you should state it: the bucket array is sized by N regardless of how few distinct values exist, so for an array of a million elements with three distinct values it allocates a million mostly-empty slots. The heap version's memory scales with the data that actually exists. Bucketing beats comparison only because the key range is known and small — remove that guarantee and the technique collapses.
Counting first: 4 appears once, 1 once, -1 twice, 2 twice, 3 once. So the map is {4:1, 1:1, -1:2, 2:2, 3:1}.
Following the heap version, processing pairs in map order:
Strip the counts off the two survivors: [-1, 2]. Both really do occur twice while everything else occurs once, and since order is free, [2, -1] would be equally correct.
Now the bucket version on the same input. Slot 1 collects [4, 1, 3] and slot 2 collects [-1, 2]; every other slot is empty. Sweep downward from slot 7: empty, empty, …, slot 2 yields -1 and then 2, and with two values collected we stop. Same answer, and notice we never looked at slot 1 at all — the elements that lose are never examined.
The heap did not change between this problem and Kth Largest; only what it ranks by changed. That is the payoff of learning a pattern as a skeleton with slots — the second problem in a family should feel like filling in a form, and the effort moves to deciding what the ranking key is.
The deeper lesson is the third approach. Heaps are comparison-based, and comparison-based selection has a log in it that cannot be removed. The moment your ranking key is a small bounded integer — a frequency, a score out of 100, an age in years, a character code — you can index instead of compare and drop the log entirely. Train that reflex: after you have a working heap solution, always ask what is the range of my key? If the answer is "small and bounded", there is a faster solution and the interviewer is probably waiting for it.