Problem
Given an integer array
numsand an integerk, return thekmost frequent elements within the array. The test cases are generated such that the answer is always unique. You may return the output in any order.
Example 1
Input: nums = [1, 2, 2, 3, 3, 3], k = 2
Output: [2, 3]
Example 2
Input: nums = [7, 7], k = 1
Output: [7]
Constraints
1 <= len(nums) <= 10^4-1000 <= nums[i] <= 10001 <= k <=number of distinct elements innums
Thinking
Let = len(nums), = number of distinct values (, and here ).
- Brute force idea: count every value, then sort the distinct values by count and take the first : . That’s already fine; the follow-up asks for better than .
- Key insight: step 1 is always count with a hash map (). Step 2 is “pick the top
by count”, and there are three ways to do it:
- heap of size : ;
- bucket sort: a count can only be between 1 and , so put each value in
buckets[count]and walk from the highest count down: ; - full sort: .
- Unique answer guarantee: the bucket that takes you past can never have a tie, so you can
add whole buckets with
extendand stop whenlen(result) >= k. - Edge cases to remember: one element (
[7]), every element the same (count , so the bucket list needs index ), = number of distinct values, negative numbers.
Solution
Approach A — my version (bucket sort, after fixing the two bugs)
Complexity: Time · Space . The Big-O is already optimal. It’s slow in practice because of constant factors:
n + 1buckets, sized bylen(nums). With that is 10,001 Python list objects (~700 KiB), built in a Python loop, even though the highest count is often around 5. This is the main memory cost and about a quarter of the runtime.- The bucket walk never stops. After
resulthas elements theifkeeps failing, but the loop still visits all 10,001 buckets.temp[::-1]also copies the whole list first. - Counting in a Python loop.
frequency[num] += 1runs times in bytecode;Counter(nums)does the same counting in C, about 2× faster. int(x)is left over from the oldstrkeys and does nothing now.
The code is unchanged except for the signature and comments.
from collections import Counter, defaultdict
import heapq
def top_k_frequent_mine(nums: list[int], k: int) -> list[int]:
frequency = defaultdict(int)
temp = [[] for _ in range(len(nums) + 1)] # SLOW: n+1 lists, most of them stay empty
for num in nums:
frequency[num] += 1 # SLOW: counting loop in Python, Counter is C
for x, y in frequency.items():
temp[y].append(int(x)) # int() does nothing now
result = []
for z in temp[::-1]: # copies the list, then visits every bucket
if len(result) < k:
result.extend(z) # no break: keeps looping after it has k
return result
Approach B — bucket sort, done efficiently (the interview answer)
Complexity: Time · Space worst case, but only in practice.
Same idea as mine with three fixes: count with Counter, make only max count + 1 buckets, and
break once there are results. This alone makes it ~3× faster and cuts peak memory ~7×.
def top_k_frequent_bucket(nums: list[int], k: int) -> list[int]:
count = Counter(nums)
buckets = [[] for _ in range(max(count.values()) + 1)] # index = count
for num, c in count.items():
buckets[c].append(num)
result = []
for bucket in reversed(buckets): # highest count first, no copy
result.extend(bucket)
if len(result) >= k:
break
return result
Approach C — heap of size (heapq.nlargest)
Complexity: Time · Space .
Count, then keep only the largest counts in a min-heap. nlargest does this in C. It’s the
answer to give when the data is a stream or is much smaller than .
def top_k_frequent_heap(nums: list[int], k: int) -> list[int]:
count = Counter(nums)
return heapq.nlargest(k, count, key=count.get)
Approach D — Counter.most_common ✅ best practice (Pythonic)
Complexity: Time · Space .
most_common(k) calls heapq.nlargest internally, so this is Approach C in one line. In
everyday Python, write this. In an interview, write it, then explain B (the bucket sort)
when asked for better than .
def top_k_frequent(nums: list[int], k: int) -> list[int]:
return [num for num, _ in Counter(nums).most_common(k)]
Scratch / Experiments
Worst-ish case at the max size: random values from the full range , so and the highest count is only ~15.
All four are or close to it, so the gaps are constant factors:
- A vs B: same algorithm. B is ~3× faster only because it counts in C, builds ~15 buckets instead of 10,001, and stops early.
- B vs C/D: bucket sort is and the heap is , but with small,
is tiny and
nlargestruns in C, so they finish about even. Big-O tells you how it grows, not which is faster at . - The second block measures peak memory: A’s 10,001 empty lists are almost all of it.
LeetCode’s runtime/memory percentiles are noisy (±20% between runs of the same code), so treat them as a hint, not a verdict.
import random, tracemalloc
random.seed(0)
nums = [random.randint(-1000, 1000) for _ in range(10_000)]
k = 10
for fn in APPROACHES:
print(fn.__name__)
%timeit fn(nums, k)
for fn in APPROACHES:
tracemalloc.start()
fn(nums, k)
peak = tracemalloc.get_traced_memory()[1]
tracemalloc.stop()
print(f"{fn.__name__:24s} peak {peak / 1024:6.0f} KiB")
Takeaways
- Pattern: “top / most frequent / largest” → count with a hash map, then
select:
Counter.most_common(k)/heapq.nlargest(), or bucket sort by count when the counts are bounded by (). - Mistakes I made:
- Sized the bucket list as
len(nums): a value can appearlen(nums)times, so I neededlen(nums) + 1(crashed on[7, 7]). - Used
<= kas “still need more”: that’s< k. Off-by-one in a stop condition: trace the example by hand and check the last iteration. - Used
str(num)as the dict key: ints are hashable, so convert nothing. - Optimal Big-O can still be slow. Mine was but allocated 10,001 lists and never
stopped looping. Size buffers by what’s needed (
max(count.values())),breakwhen done, and prefer C-implemented builtins (Counter,heapq) over hand-written loops.
- Sized the bucket list as
- Related problems: Kth Largest Element in an Array (#215, heap / quickselect for top ),
Top K Frequent Words (#692, same idea plus a tie-break by word), Sort Characters By Frequency
(#451, count + bucket sort), K Closest Points to Origin (#973,
heapq.nsmallestwith a key), Group Anagrams (#49, the counting-with-a-dict step).