Top K Frequent Elements in Java DSA: HashMap, Heap, and Bucket Sort
-
Last Updated: August 8, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Solve Top K Frequent Elements in Java DSA three ways — HashMap sort, min-heap, and O(n) bucket sort — with beginner-friendly dry runs and clean code.
Top K Frequent Elements in Java is a problem that shows up a lot in interviews. It sounds fancy, but the idea is plain. You are given a list of numbers, and you must return the k numbers that appear most often.
Say the list is [1, 1, 1, 2, 2, 3, 4] and k is 2. The number 1 shows up three times, and 2 shows up twice. So the answer is [1, 2], because those two are the most frequent.
The order of the answer does not matter here. As long as you return the correct k numbers, you are good.
We build the solution in three steps, like always. Counting with a HashMap and then sorting is the first, easy idea. A heap of size k is the next step up. The bucket sort trick is the lean version that runs in linear time and wins interviews.
Every approach gets a full, step-by-step dry run on the same list. Nothing gets skipped, so you can watch the counts grow and see exactly why each number lands in the answer.
Let us pin down the rules before writing any code.
For our example the answer is [1, 2]. The value 1 appears three times, which is the highest count. Next comes 2 with a count of two. The numbers 3 and 4 appear only once each, so they miss the cut.
The problem also promises the answer is always unique. That means you will never face a tie that makes the top k unclear.
Every approach starts the same way. You walk the array once and count how many times each number appears. A HashMap is perfect for this job.
After one pass you know the exact frequency of every number. That map is the base for all three approaches.
Once you have the counts, the real question is how to pull out the k largest. There are three common ways, and they differ in speed.
First count every number with a HashMap. Then take the map entries and sort them by count, from highest to lowest. Finally, pick the first k numbers from that sorted list. It is the most direct idea, even though sorting is more work than the problem truly needs.
count = empty map from number to its count
for num in nums:
count[num] = count[num] + 1
entries = list of (number, count) pairs from count
sort entries by count from high to low
result = first k numbers from entries
return resultThe plan reads top to bottom in three plain moves: count first, sort next, then slice off the top k. Let us look at each line and see what it does and why.
The counting loop.
Turning the map into a sortable list.
Sorting by count, high to low.
Slicing the answer.
import java.util.*;
public class TopKSort {
public static int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
List<Integer> keys = new ArrayList<>(count.keySet());
keys.sort((a, b) -> count.get(b) - count.get(a));
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = keys.get(i);
}
return result;
}
public static void main(String[] args) {
int[] nums = {1, 1, 1, 2, 2, 3, 4};
System.out.println(Arrays.toString(topKFrequent(nums, 2))); // [1, 2]
}
}This is the same plan written in Java. The one helper to know is getOrDefault, which returns 0 for a number we have not seen yet, so we never hit a null.
Let us trace nums = [1, 1, 1, 2, 2, 3, 4] with k = 2. First we build the count map in one pass. The table shows the map after reading each number.
| i (read index) | num | map after this step |
|---|---|---|
| 0 | 1 | {1:1} |
| 1 | 1 | {1:2} |
| 2 | 1 | {1:3} |
| 3 | 2 | {1:3, 2:1} |
| 4 | 2 | {1:3, 2:2} |
| 5 | 3 | {1:3, 2:2, 3:1} |
| 6 | 4 | {1:3, 2:2, 3:1, 4:1} |
Now we take the distinct numbers [1, 2, 3, 4] and sort them by count from high to low. The next table shows the order settling as the comparator compares counts.
| number | its count | position after sort |
|---|---|---|
| 1 | 3 | 1st (highest count) |
| 2 | 2 | 2nd |
| 3 | 1 | 3rd |
| 4 | 1 | 4th |
The sorted list of numbers is [1, 2, 3, 4]. We take the first k = 2 of them, which gives the answer [1, 2].
Let us walk the trace in two parts and see what each part buys us.
The counting pass over nums.
This pass just tallies how often each number appears. The map starts empty and grows one entry at a time as we read left to right. Read the last table column as a photo of the map right after each number is processed.
After the whole pass the map reads {1:3, 2:2, 3:1, 4:1}. That is the frequency fingerprint of the array: 1 appeared three times, 2 twice, and both 3 and 4 once each. Every later step in every approach builds on top of this same map.
The sorting pass over the counts.
Now we take the four distinct numbers and sort them by their count, largest first. The one thing to hold in your head: the sort compares counts, never the numbers themselves. A big number with a small count still loses.
The sorted list of numbers is [1, 2, 3, 4]. We slice off the first k = 2 entries, which are 1 and 2, and return them. Sorting worked, but look closely: we ordered all four numbers just to keep the top two. Half that work was wasted. That wasted effort is exactly what the heap and bucket approaches trim away.
| 💡 Interview Insight A common opener is “what is the time cost of this approach?” Say O(n log n), because sorting the counts dominates. That answer sets you up to say you can do better. |
Sorting is clean and short, but that log n factor is wasted effort. We only wanted the top k, not a fully ordered list. So next we keep just the best k with a heap.
Count every number first, same as before. Then push each (count, number) pair into a min-heap, but never let the heap grow past size k. Whenever it holds more than k pairs, pop the smallest count out. After all pairs go through, the heap holds exactly the k most frequent numbers. This skips sorting the whole thing.
count = empty map from number to its count
for num in nums:
count[num] = count[num] + 1
heap = empty min-heap ordered by count
for (number, cnt) in count:
push (cnt, number) into heap
if size of heap > k:
pop the smallest from heap
result = every number left in heap
return resultFirst, a quick word on what a min-heap is, because the whole approach rests on it. A min-heap is a collection that always keeps its smallest item on top and ready to remove. Here we order by count, so the number with the lowest count is always the one sitting on top. Picture a small box that holds only k pairs at a time.
Pushing a pair.
Guarding the size.
Collecting the answer.
import java.util.*;
public class TopKHeap {
public static int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// min-heap ordered by count (smallest count on top)
PriorityQueue<int[]> heap =
new PriorityQueue<>((a, b) -> a[1] - b[1]);
for (Map.Entry<Integer, Integer> e : count.entrySet()) {
heap.offer(new int[]{e.getKey(), e.getValue()});
if (heap.size() > k) {
heap.poll();
}
}
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = heap.poll()[0];
}
return result;
}
public static void main(String[] args) {
int[] nums = {1, 1, 1, 2, 2, 3, 4};
System.out.println(Arrays.toString(topKFrequent(nums, 2))); // [1, 2] in some order
}
}The Java version uses a PriorityQueue, which is Java’s built-in heap. Each item is a small int array where slot 0 is the number and slot 1 is its count.
We reuse the same map {1:3, 2:2, 3:1, 4:1} and k = 2. We walk its entries in the order 1, 2, 3, 4 to keep the trace clear. The heap keeps its smallest count on top, ready to be popped.
| step | pair pushed (num, cnt) | size > k? | action | heap after step (num:cnt) |
|---|---|---|---|---|
| 1 | (1, 3) | no (size 1) | push only | 1:3 |
| 2 | (2, 2) | no (size 2) | push only | 2:2, 1:3 |
| 3 | (3, 1) | yes (size 3) | push, then pop top 3:1 | 2:2, 1:3 |
| 4 | (4, 1) | yes (size 3) | push, then pop top 4:1 | 2:2, 1:3 |
After every pair is processed, the heap holds 2:2 and 1:3. We drain it into the answer, giving [2, 1]. The order differs from the sort approach, but both numbers are correct.
Let us go step by step and watch the small heap fill up and then guard its own size. Keep k = 2 in mind: the heap is never allowed to hold more than two pairs for long. Remember the rule of a min-heap, the smallest count is always on top, first in line to be removed.
First step, push (1, 3).
The heap starts empty, so the pair 1:3 goes straight in with no fuss. It now holds one pair, which is not over k, so nothing gets popped. Right now the top is 1:3, because it is the only pair there.
Next step, push (2, 2).
Now the pair 2:2 enters. The heap compares counts and sees that 2 is smaller than 3.
Third step, push (3, 1), then pop.
The pair 3:1 goes in. Its count of 1 is the smallest so far, so it floats to the very top. Now the heap holds three pairs, which is over k, so the guard fires and we pop the top.
Last step, push (4, 1), then pop.
The final pair 4:1 enters. Just like 3:1 before it, its count of 1 is the smallest, so it lands on top. The size is 3 again, over k, so we pop once more.
Look at what happened across the whole run: we never sorted all four numbers. The heap held at most k+1 pairs at any instant, and it quietly threw away the weak ones the moment they made it too big. Each push or pop only shuffles a heap of size k, which is cheap. That is why the heap beats a full sort when k is small.
| 💡 Interview Insight If asked “why a min-heap and not a max-heap?” explain that a min-heap of size k keeps the smallest count on top, so the weakest candidate is always the one you pop. That is exactly the one you want to drop. |
This beats the full sort when k is much smaller than the number of distinct values. Still, there is one more trick that drops the time all the way to linear.
Here is the clever part. A number in an array of size n can appear at most n times. So we make n+1 buckets, one for each possible count from 0 to n. We drop each number into the bucket that matches its count. Then we scan buckets from the highest count downward and collect numbers until we have k. No sorting, no heap, just plain array indexing. This is the answer interviewers hope to see.
count = empty map from number to its count
for num in nums:
count[num] = count[num] + 1
buckets = array of (n + 1) empty lists // index = frequency
for (number, cnt) in count:
add number to buckets[cnt]
result = empty list
for freq from n down to 1:
for number in buckets[freq]:
add number to result
if size of result == k:
return resultThe whole trick is one idea: use the count itself as an array index. Bucket number 3 holds every number that appeared exactly three times. Bucket number 1 holds every number that appeared once. Because a number can show up at most n times in an array of size n, we make n+1 buckets, one for each count from 0 to n.
Counting, then building empty buckets.
Filling the buckets.
Scanning from the top down.
import java.util.*;
public class TopKBucket {
public static int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// index = frequency, value = numbers with that frequency
List<Integer>[] buckets = new List[nums.length + 1];
for (Map.Entry<Integer, Integer> e : count.entrySet()) {
int freq = e.getValue();
if (buckets[freq] == null) {
buckets[freq] = new ArrayList<>();
}
buckets[freq].add(e.getKey());
}
int[] result = new int[k];
int idx = 0;
for (int freq = nums.length; freq >= 1 && idx < k; freq--) {
if (buckets[freq] != null) {
for (int num : buckets[freq]) {
result[idx++] = num;
if (idx == k) {
break;
}
}
}
}
return result;
}
public static void main(String[] args) {
int[] nums = {1, 1, 1, 2, 2, 3, 4};
System.out.println(Arrays.toString(topKFrequent(nums, 2))); // [1, 2]
}
}The code looks long, but each part is simple. The buckets array does the heavy lifting, and the count value becomes the array index.
Same input, nums = [1, 1, 1, 2, 2, 3, 4], k = 2, and the count map {1:3, 2:2, 3:1, 4:1}. The array length is 7, so we make 8 buckets, indexed 0 through 7. First we fill the buckets by walking the map entries in the order 1, 2, 3, 4.
| step | num | its count | bucket updated | buckets so far (index:list) |
|---|---|---|---|---|
| 1 | 1 | 3 | buckets[3].add(1) | 3:[1] |
| 2 | 2 | 2 | buckets[2].add(2) | 2:[2], 3:[1] |
| 3 | 3 | 1 | buckets[1].add(3) | 1:[3], 2:[2], 3:[1] |
| 4 | 4 | 1 | buckets[1].add(4) | 1:[3,4], 2:[2], 3:[1] |
Now we scan the buckets from index 7 down to 1 and collect numbers until we hold k = 2. Empty buckets are skipped.
| freq (bucket index) | bucket contents | action | result after step |
|---|---|---|---|
| 7 | empty | skip | [] |
| 6 | empty | skip | [] |
| 5 | empty | skip | [] |
| 4 | empty | skip | [] |
| 3 | [1] | take 1 | [1] |
| 2 | [2] | take 2 (now size 2 = k) | [1, 2] |
We reached k numbers at frequency 2, so we stop right there. The answer is [1, 2], and we never even looked at bucket 1.
Let us walk the two phases and see why plain array indexing beats sorting here. Recall the setup: the array length is 7, so we have 8 buckets numbered 0 through 7, and the index of a bucket is the frequency it stores.
The bucket-fill phase.
Each number drops into the bucket that matches its count. The count is used directly as the index, so there are no comparisons at all, just a jump to the right slot.
After filling, three buckets have numbers: bucket 3 holds [1], bucket 2 holds [2], and bucket 1 holds [3, 4]. The higher buckets 4, 5, 6, and 7 stay empty, because no number in our array appeared that many times. Those empty buckets are fine, we will just skip over them.
The scan phase, high to low.
Now we read buckets from the top index downward. Because higher indexes mean higher frequency, the most frequent numbers come out first. We collect numbers into the result and stop the very moment we have k of them.
Since the result now holds k = 2 numbers, we stop immediately. We never even touched bucket 1, so the numbers 3 and 4 were never considered for the answer. Count each phase once: one pass to count, one pass to fill buckets, and at most one pass to scan. No phase ever sorts, so nothing costs log n. That early stop, plus zero sorting, is why bucket sort runs in linear time.
| 💡 Interview Insight Interviewers love to ask “how is this O(n) when the last approaches were slower?” The reason is simple: counting is one pass, filling buckets is one pass, and scanning buckets is at most one more pass. No step ever sorts, so nothing costs log n. |
| Approach | How it picks top k | Extra memory | Speed feel |
|---|---|---|---|
| Count then sort | Sort all counts, slice k | Map plus list of numbers | Slowest of the three |
| Min-heap of size k | Keep only best k, pop rest | Map plus heap of size k | Fast, log k per number |
| Bucket sort | Index by count, scan top down | Map plus n+1 buckets | Fastest, linear time |
All three return the correct top k numbers. They just pay different prices to get there.
| Approach | Time | Space | Note |
|---|---|---|---|
| Count then sort | O(n log n) | O(n) | Simple, but sorts more than needed |
| Min-heap of size k | O(n log k) | O(n + k) | Great when k is small |
| Bucket sort | O(n) | O(n) | Fastest, the expected answer |
The heap wins over the full sort when k is small, because log k is smaller than log n. But bucket sort skips comparisons entirely, so it reaches linear time.
In an interview, mention the sort first to show you understand the problem. Then bring up the heap as a faster option for small k. Finally, land on bucket sort as the linear-time answer. That progression tells a clean story.
| 💡 Interview Insight If pushed on why bucket sort is O(n) space and not less, point out the n+1 buckets. The buckets array grows with the input size, so it counts as linear extra space, even though the scan itself is quick. |
A few small traps catch beginners on Top K Frequent Elements. Keep them in mind.
Run a case where k equals the distinct count, and a case with a single repeated number, before you call your code done. These catch more bugs than any ordinary input will.
A: Bucket sort is the fastest, running in O(n) time. You count each number with a HashMap, drop each number into a bucket indexed by its frequency, then scan buckets from the highest count down until you collect k numbers. No sorting or heap is needed.
A: A min-heap of size k keeps the smallest count on top, so the weakest candidate is always the one you pop when the heap grows past k. This keeps only the k strongest numbers and costs O(n log k), which beats sorting everything when k is small.
A: The buckets array must have n + 1 slots, where n is the array length. A number can appear at most n times, so you need a bucket for every possible frequency from 0 to n. Sizing it smaller will cause an index error for the most frequent element.
A: No. Any order is accepted, so [1, 2] and [2, 1] are both correct answers. That is why the heap approach can return the numbers in a different order than the sort approach while still being right.
A: Count then sort is O(n log n). The min-heap of size k is O(n log k). Bucket sort is O(n), which is the best of the three, because counting, filling buckets, and scanning are each a single pass with no comparisons.
Top K Frequent Elements in Java looks tricky at first, yet it rests on one habit you will reuse everywhere: count first, then decide how to pick the top. Once you see the array as a bag of counts, the whole problem clicks.
Our seven-number trace showed the payoff clearly. Sorting ordered every count just to keep two. The heap kept only the best k and threw the rest away. Bucket sort used the count as an index and scanned from the top, reaching linear time.
So take the pattern, not just the answer. When you need the largest k of something, a heap of size k or a bucket by value will usually beat a full sort. Reach for buckets when the values fit a small range, and keep the heap handy when k is small.
That counting habit turns many array and hashing problems from tricky into routine. It will serve you well on the harder problems waiting further down the list.