Solving tips
- Step one is always a hash-map count; the interesting part is selecting the top k without a full O(n log n) sort.
- For true O(n), bucket by frequency: buckets[f] holds values occurring f times (f ranges 1..n), then sweep f from n down to 1 collecting values until you have k.
- A size-k min-heap gives O(n log k) and is the go-to when k is small or data streams; keep the k largest by evicting the root (smallest).
- Pitfall: size the bucket array to n+1 (a value can occur n times), and use a MIN-heap so you evict the smallest frequency.
Problem
Given an integer array nums and an integer k, return the k values that occur most often in nums. The answer may be returned in any order, and it is guaranteed to be unique (no ties that would make the choice ambiguous).
Follow-up: your algorithm should beat O(n log n) β i.e. do better than βsort everything by frequencyβ.
Examples
Example 1: nums = [1, 1, 1, 2, 2, 3], k = 2 β [1, 2]
1 appears three times and 2 twice; 3βs single occurrence loses.
Example 2: nums = [1], k = 1 β [1]
Only one distinct value.
Example 3: nums = [4, 4, 4, 5, 5, 6], k = 1 β [4]
4 is the unique most frequent element.
Constraints
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
k is between 1 and the number of distinct elements; the answer is unique.
n up to 10^5 makes counting cheap; the interesting part is selecting the top k without a full O(n log n) sort.
Think about it first
Hint 1
Step one is always the same: how do you get every element's frequency in one pass?
Hint 2
You need the k largest frequencies, not a full ranking. Which data structure extracts "k largest" while only ever holding k items?
Hint 3
For true O(n): a frequency can only be 1..n. Make a bucket per frequency (`buckets[f]` = values occurring f times), then read buckets from n down to 1 until you've collected k values β an application of counting/bucket sort.
TL;DR
Count with a hash map, then bucket-by-frequency β O(n) time, O(n) space (heap variant: O(n log k)).
Approach 1 β Brute force (count, then sort all by frequency)
The naive intuition: tally frequencies with a hash map, sort every distinct value by its count, take the top k.
from typing import List
from collections import Counter
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
count = Counter(nums)
ranked = sorted(count, key=lambda v: count[v], reverse=True)
return ranked[:k]
Complexity: O(n) to count + O(d log d) to sort d distinct values β O(n log n) worst case, O(n) space.
It passes at n = 10^5, but it fully ranks all d values when only the top k are wanted β exactly what the follow-up says to beat.
Approach 2 β Min-heap of size k
The insight: to keep the k largest frequencies you never need more than k candidates at once. A min-heap (a binary heap where the smallest element sits at the root, O(log size) push/pop) of size k holds the current best k; any newcomer beating the root evicts it. Each of the d distinct values costs O(log k), not O(log d).
from typing import List
from collections import Counter
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
count = Counter(nums)
heap: List[tuple[int, int]] = [] # (frequency, value), min at root
for value, freq in count.items():
heapq.heappush(heap, (freq, value))
if len(heap) > k:
heapq.heappop(heap) # evict current smallest
return [value for _, value in heap]
Walkthrough on Example 1, nums = [1,1,1,2,2,3], k = 2:
- Counts:
{1: 3, 2: 2, 3: 1}.
- Push (3, 1) β heap
[(3,1)].
- Push (2, 2) β heap
[(2,2), (3,1)] (size 2, at capacity).
- Push (1, 3) β size 3 > k, pop the root (1, 3) β the smallest frequency β leaving
[(2,2), (3,1)].
- Extract values:
[2, 1] β same set as expected [1, 2] (any order allowed). β
Complexity: O(n + d log k) time, O(d) space. This is the go-to when k βͺ d or when data arrives as a stream.
Approach 3 β Bucket sort by frequency
The insight: frequencies live in the bounded range 1..n, so instead of comparing them, index by them β the essence of bucket/counting sort (group items by a small-integer key, then read groups in key order). buckets[f] collects all values occurring exactly f times; sweeping f from n down to 1 yields values in non-increasing frequency order with no comparison sort at all.
from typing import List
from collections import Counter
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
count = Counter(nums)
buckets: List[List[int]] = [[] for _ in range(len(nums) + 1)]
for value, freq in count.items():
buckets[freq].append(value)
result: List[int] = []
for freq in range(len(nums), 0, -1):
for value in buckets[freq]:
result.append(value)
if len(result) == k:
return result
return result # unreachable given valid k
Walkthrough on Example 1, nums = [1,1,1,2,2,3], k = 2:
- Counts:
{1: 3, 2: 2, 3: 1}; n = 6, so buckets indexed 0..6.
- Fill:
buckets[3] = [1], buckets[2] = [2], buckets[1] = [3], others empty.
- Sweep f = 6, 5, 4: empty. f = 3: take 1 (result
[1]). f = 2: take 2 β size k, return [1, 2]. β
Complexity: O(n) time (count, fill, sweep are each linear), O(n) space for the buckets.
Common pitfalls
- Sizing the bucket array to
d (distinct count) instead of n + 1 β a value can occur up to n times, so frequency n must have a bucket.
- Using a max-heap of size k: to keep the k largest, you evict the smallest, which needs a min-heap; a max-heap works only if you push all d items and pop k times (O(d + k log d), also fine but a different scheme).
- Pushing raw values into the heap instead of
(freq, value) pairs β the heap must be ordered by frequency.
- Sweeping buckets from low to high frequency, which returns the least frequent elements.
Pattern takeaway
βTop-k by some scoreβ decomposes into: hash-map counting, then a selection structure. A size-k min-heap gives O(n log k) and works for streams; when the score is a bounded small integer (a count canβt exceed n), buckets indexed by score replace comparison sorting entirely and reach O(n). Recognizing βthe key is a bounded int β index by itβ is the recurring arrays-and-hashing move.