DSA Top K Frequent Elements
Top K Frequent Elements finds the k values that appear most often in a list. A frequency map counts the values first.
The problem
Top K Frequent Elements finds the k values that appear most often in a list. The answer lists those values from the highest count down.
Equal counts need one fixed tie rule. This article orders tied values from smallest to largest. That rule keeps the printed answer the same on every run.
The cases used in this article
cases = [
([1, 1, 1, 2, 2, 3], 2, [1, 2]),
([4, 4, 5, 5, 6, 6], 2, [4, 5]),
([7], 1, [7]),
([1, 2, 3], 3, [1, 2, 3]),
]
for nums, k, expected in cases:
print("nums", nums, "k", k, "->", expected)The second case is a three-way tie at two appearances each. The tie rule takes 4 and then 5. The last case takes every value, because k equals the number of distinct values.
Intuition: grab the biggest count, again and again
A first idea counts the values once. Then it scans the counts for the largest one and takes that value. It repeats the scan until k values are taken.
The idea works. Its cost hides in the repeats. Each new answer value pays a fresh scan over every distinct value still waiting.
Repeated scans for the top two
freq = {1: 3, 2: 2, 3: 1}
taken = []
scans = 0
for _ in range(2):
best = None
for value in sorted(freq):
scans += 1
if value not in taken and (best is None or freq[value] > freq[best]):
best = value
taken.append(best)
print("top 2:", taken)
print("count entries scanned:", scans)Two answers cost two full scans here. Ten answers over many distinct values cost ten scans. The counting work was done once, and then the ranking pays again and again.
Brute force: repeated maximum scans, counted
The brute force writes the intuition as a function. It counts first. Then each round scans every remaining entry for the largest count, with ties going to the smallest value.
The brute force as a function
def top_k_brute(nums, k):
freq = {}
for num in nums:
freq[num] = freq.get(num, 0) + 1
taken = []
scans = 0
for _ in range(min(k, len(freq))):
best = None
for value in sorted(freq):
if value in taken:
continue
scans += 1
if best is None or freq[value] > freq[best]:
best = value
taken.append(best)
return taken, scans
result, scans = top_k_brute([1, 1, 1, 2, 2, 3], 2)
print("top 2:", result)
print("entries scanned:", scans)
result, scans = top_k_brute([4, 4, 5, 5, 6, 6], 2)
print("tie case:", result)
print("entries scanned:", scans)Each round scans the entries that are still in play. The scan count grows with both k and the distinct count. A full sort of the counts would also work, and it pays a sorting cost the buckets below avoid.
The optimal solution: place each value in the bucket named by its count
A frequency bucket is a list that holds every value with one given count. The bucket index is the count itself. Reading the buckets from the highest count down gives the ranking in one walk.
No count can pass the number of items in the list. A bucket list with one slot per item plus one is always long enough.
The bucket solution on every case
def top_k(nums, k):
if k <= 0 or not nums:
return []
freq = {}
for num in nums:
freq[num] = freq.get(num, 0) + 1
buckets = [[] for _ in range(len(nums) + 1)]
for value, count in freq.items():
buckets[count].append(value)
result = []
for count in range(len(buckets) - 1, 0, -1):
for value in sorted(buckets[count]):
result.append(value)
if len(result) == k:
return result
return result
cases = [
([1, 1, 1, 2, 2, 3], 2, [1, 2]),
([4, 4, 5, 5, 6, 6], 2, [4, 5]),
([7], 1, [7]),
([1, 2, 3], 3, [1, 2, 3]),
]
for nums, k, expected in cases:
result = top_k(nums, k)
print("nums", nums, "k", k, "->", result, "match:", result == expected)All four cases pass. Sorting happens only inside one bucket at a time, and only to fix the order of ties.
Walkthrough: [1, 1, 1, 2, 2, 3] with k at 2
The walk counts first. The state then becomes the bucket list. The answer is read straight off the buckets from the top.
Counts, buckets, then the read-off
nums = [1, 1, 1, 2, 2, 3]
freq = {}
for num in nums:
freq[num] = freq.get(num, 0) + 1
print("counts:", {v: freq[v] for v in sorted(freq)})
buckets = [[] for _ in range(len(nums) + 1)]
for value in sorted(freq):
buckets[freq[value]].append(value)
for count, bucket in enumerate(buckets):
if bucket:
print("bucket", count, "holds", bucket)
top = []
for count in range(len(buckets) - 1, 0, -1):
top.extend(sorted(buckets[count]))
print("read from the top:", top[:2])Bucket 3 holds the value 1. Bucket 2 holds the value 2. The first two values read from the top are the answer, and no further bucket is opened.
Complexity
The brute force scans the distinct entries once per answer value, so its ranking time grows with k times the distinct count. The bucket walk places each distinct value once and reads each bucket once, so its time is O(n) for n items.
The bucket list holds one slot per item plus one, and the buckets together hold each distinct value once, so the extra space is O(n). The frequency map also holds one entry per distinct value.
Counting the work instead of timing it
for distinct in [3, 6, 12]:
k = distinct
print("distinct", distinct, "- brute scans up to:", k * distinct, " bucket placements:", distinct)When k grows with the distinct count, the brute scans grow quadratically. Placements grow in a straight line. Counting first turns a ranking problem into a bucket walk.
Edge cases and pitfalls
A k of zero asks for no values. A k beyond the distinct count returns every distinct value in ranked order. An empty list returns an empty answer.
Edge cases, run
def top_k(nums, k):
if k <= 0 or not nums:
return []
freq = {}
for num in nums:
freq[num] = freq.get(num, 0) + 1
buckets = [[] for _ in range(len(nums) + 1)]
for value, count in freq.items():
buckets[count].append(value)
result = []
for count in range(len(buckets) - 1, 0, -1):
for value in sorted(buckets[count]):
result.append(value)
if len(result) == k:
return result
return result
print("k is zero:", top_k([1, 1, 2], 0))
print("empty list:", top_k([], 3))
print("k past distinct:", top_k([2, 2, 1], 5))
print("all the same:", top_k([9, 9, 9], 1))
print("negative values:", top_k([-1, -1, 0], 2))The classic pitfall is an unstable tie. Two runs could order equal counts differently if the tie order came from dict order alone. Sorting inside each bucket removes that risk.
Exercise
A value appears 5 times in a list of 40 items. Where does it wait before the ranking is read?