HARD Lab · Heap / Priority Queue · 76

347. Top K Frequent Elements

Return the k values that appear most often. Count with a hash map, then either sift the top k with a heap in O(n log k) — or skip sorting entirely and bucket by frequency for a clean O(n) answer.

MediumBucket SortHash MapHeapTypeScript

PROBLEM What we're solving

Given an integer array nums and an integer k, return the k most frequently occurring values (in any order). Example: nums = [1,1,1,2,2,3], k = 2 → the value 1 appears 3×, 2 appears 2×, 3 appears 1×, so the answer is [1, 2]. The problem guarantees the answer is unique.

KEY IDEA Frequency is bounded by n — so it can be an index

Insight → a value can appear at most n times, so every frequency lives in the range 1…n. That means you can use the count itself as an array index: put each value into buckets[count]. Reading the buckets from high index to low gives values in descending frequency order — no comparison sort, no heap needed.

RECIPE Count, bucket by frequency, harvest top down

  • 1 · Count. One pass builds a Map<value, count>so you know each value's frequency.
  • 2 · Bucket. Create n + 1 empty lists. For each (value, count), append the value to buckets[count] — the index is the frequency.
  • 3 · Harvest. Walk buckets from the highest index down to 1, collecting values until you have k of them, then stop. Higher index = more frequent, so you naturally take the top k first.
Classic confusion → the bucket array is indexed by frequency, not by value. buckets[3]means "all values that occurred 3 times," and a single bucket can hold several values (ties). Size it to n + 1 because a count can be as large as n and index 0 is never used.

COST Heap vs bucket sort

Min-heap of size k
O(n log k)
Push each distinct freq, evict when size > k.
Bucket sort by frequency
O(n)
Counts are bounded by n, so no sort is needed.

Two valid approaches

Heap: count frequencies, then push (value) keyed by frequency into a min-heap capped at size k; whenever it overflows, pop the smallest. The heap ends holding exactly the k most frequent values, in O(n log k)time. It's the natural "top k" tool and is easy to describe in an interview. A full sort of distinct frequencies would be O(n log n).

Bucket sort: because every frequency is an integer in 1…n, you can bucket by count and read off the top k in O(n) time and O(n) space — asymptotically optimal since you must at least read the input.

Pattern transfer →"value bounded by n ⇒ use it as an index" is the core of counting sort and powers Sort Characters by Frequency, First Unique Character, and H-Index. The size-k min-heap pattern generalizes to Kth Largest Element, K Closest Points to Origin, and Merge K Sorted Lists.

RUN IT Count, bucket by frequency, harvest top down

step 0 / 14
STARTFind the 2 most frequent values in an array of 6 numbers. Phase 1: build a frequency map in one pass.
nums101112232435
current itemfrequency mapbucket (by count)result
slowfast

TYPESCRIPT The solution, annotated

topKFrequent.ts
function topKFrequent(nums: number[], k: number): number[] {
  // 1) Count every value in one pass.
  const freq = new Map<number, number>();
  for (const n of nums) {
    freq.set(n, (freq.get(n) ?? 0) + 1);
  }

  // 2) Bucket sort: buckets[c] = all values seen exactly c times.
  //    A value's count is at most nums.length, so we need that many slots.
  const buckets: number[][] = Array.from({ length: nums.length + 1 }, () => []);
  for (const [val, count] of freq) {
    buckets[count].push(val);
  }

  // 3) Walk buckets from the highest count down, collecting k values.
  const res: number[] = [];
  for (let c = buckets.length - 1; c >= 1 && res.length < k; c--) {
    for (const val of buckets[c]) {
      res.push(val);
      if (res.length === k) return res;
    }
  }
  return res;
}

Reading it block by block

Lines 2–6 — count. A single pass over nums builds a Map from each value to how many times it appears. The ?? 0 seeds first-time keys.
Lines 9–12 — bucketize. Allocate nums.length + 1 empty arrays. For each (val, count), push val into buckets[count]. We never use index 0 (no value appears zero times), and the max possible count is nums.length.
Lines 15–22 — harvest top down. Iterate the bucket indices from highest to lowest. The first non-empty high bucket holds the most frequent values. Collect until res.length === k, then return immediately — no need to scan the rest.
Why this is correct. Higher bucket index ⇒ strictly higher frequency, so walking indices downward yields values in non-increasing frequency order. Because the answer is guaranteed unique, the first k we collect are exactly the top k.
Complexity → Counting is O(n); building and reading the buckets is O(n); total O(n) time and O(n) space. The heap alternative is O(n log k) time, O(n + k) space — competitive when k is small relative to the number of distinct values.

INTERVIEWFollow-ups they'll ask

  • "Do it with a heap instead." Count, then push distinct values into a min-heap keyed by frequency; pop when size exceeds k. O(n log k).
  • "What if k equals the number of distinct values?" Both approaches just return every distinct value; the bucket walk collects them all.
  • "Return them sorted by frequency?" The bucket walk already emits descending-frequency order; within a bucket, order is arbitrary (ties).
  • "Streaming / unbounded data?" The bounded-frequency trick breaks; keep a running count map plus a size-k heap, or use Count-Min Sketch for approximate top-k.
  • "Why size buckets at n + 1?" A value can occur up to n times, so the largest index you might write is n; index 0 stays unused.

MNEMONIC The one-liner

"Count it, bucket it by how-many, scoop from the top until you have k."

TRIGGERS When you see ___ → reach for ___

"k most frequent / top k"count map → size-k heap (O(n log k))
want strictly O(n)bucket by frequency (counts ≤ n)
frequency used as an indexbuckets[count], walk high → low
"kth largest / k closest"size-k min-heap, evict smallest

SKELETON The reusable shape

skeleton.ts
const freq = new Map<number, number>();
for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1);

const buckets: number[][] = Array.from({ length: nums.length + 1 }, () => []);
for (const [v, c] of freq) buckets[c].push(v);

const res: number[] = [];
for (let c = buckets.length - 1; c >= 1 && res.length < k; c--) {
  for (const v of buckets[c]) {
    res.push(v);
    if (res.length === k) return res;
  }
}
return res;

FLASHCARDS Tap to flip

Two standard approaches to Top K Frequent?
Size-k min-heap (O(n log k)) or bucket sort by frequency (O(n)).
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
Optimal time complexity for Top K Frequent Elements?
QUESTION 02
In the bucket-sort solution, buckets[c] contains:
QUESTION 03
How large must the bucket array be?
QUESTION 04
Why walk the buckets from the highest index downward?
QUESTION 05
In the min-heap approach, what gets evicted when the heap exceeds size k?
QUESTION 06
For nums = [4,4,4,5,5,6,6,6,6], k = 1, the answer is:
QUESTION 07
What primarily makes the heap approach O(n log k) rather than O(n log n)?