Topics › Heaps › Hard Problems
Top K Frequent Elements
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given an integer array nums and an integer k, return the k values that occur most often. The answer may be in any order. The input guarantees that the answer is unique: there is never a tie for the last place in the top k.
Constraints: 1 ≤ nums.length ≤ 10^5, -10^4 ≤ nums[i] ≤ 10^4, and k is between 1 and the number of distinct values. The follow-up asks for something better than O(n log n).
Example 1
nums = [1, 1, 1, 2, 2, 3], k = 2
answer = [1, 2] // 1 occurs 3 times, 2 occurs twice, 3 once
Example 2
nums = [4, 4, -1, -1, -1, 2], k = 1
answer = [-1] // -1 occurs 3 times, more than any other value
Every approach starts the same way: one pass with a HashMap from value to count. Write d for the number of distinct values. The approaches differ in how they pick the k largest counts out of those d entries.
Approach 1: Brute force, count and sort
Intuition. Once we know every value’s count, “the k most frequent” is “the first k after sorting by count, descending”. So put the map’s entries into a list, sort it with a comparator on the count, and take the first k keys. It is short and correct, and it does more work than necessary, because it fully orders all d values when we only need the top k.
Algorithm.
- Count occurrences with
freq.merge(x, 1, Integer::sum). - Copy
freq.entrySet()into a list and sort it by value, descending. - Return the keys of the first
kentries.
static int[] bruteForce(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int x : nums) freq.merge(x, 1, Integer::sum);
List<Map.Entry<Integer, Integer>> entries = new ArrayList<>(freq.entrySet());
entries.sort((p, q) -> Integer.compare(q.getValue(), p.getValue())); // most frequent first
int[] res = new int[k];
for (int i = 0; i < k; i++) res[i] = entries.get(i).getKey();
return res;
}
Complexity. Time O(n + d log d): one counting pass plus sorting d entries. In the worst case d = n, so O(n log n). Space O(d).
Approach 2: Better, size-k min-heap
Intuition. This is the standard “top k” pattern. Keep a heap of at most k candidates, ordered by count, with the least frequent candidate on top. Offer each distinct value. If the heap grows to k + 1, evict the top, which is the weakest candidate. A value that is evicted had at least k values with a higher or equal count, so it cannot be in the answer. When every value has been offered, the heap holds exactly the top k.
The heap stores values, and the comparator looks their counts up in freq. The map is never reassigned, so the lambda can capture it.
Algorithm.
- Count occurrences.
- For each distinct value
x: pushx, and if the heap size exceedsk, poll. - Poll the remaining
kvalues into the result. Filling from the back gives the most frequent first, though any order is accepted.
static int[] better(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int x : nums) freq.merge(x, 1, Integer::sum);
PriorityQueue<Integer> pq = new PriorityQueue<>((p, q) -> Integer.compare(freq.get(p), freq.get(q))); // least frequent on top
for (int x : freq.keySet()) {
pq.add(x);
if (pq.size() > k) pq.poll(); // evict the weakest candidate
}
int[] res = new int[k];
for (int i = k - 1; i >= 0; i--) res[i] = pq.poll();
return res;
}
Complexity. Time O(n + d log k): each of the d offers costs O(log k) because the heap never holds more than k + 1 items. Space O(d + k).
When k is small, this is effectively linear. When k is close to d, it is no better than sorting. The follow-up wants an approach that does not depend on k.
Approach 3: Optimal, bucket sort by frequency
Intuition. Counts are not arbitrary numbers. Every count is an integer between 1 and n. When the keys being sorted come from a small known range, comparison sorting is unnecessary: make one bucket per possible count, drop each value into the bucket for its count, then read the buckets from the highest count down until k values have been collected.
buckets.get(f) holds every value that occurs exactly f times. Walking f from n down to 1 visits values in non-increasing order of frequency, which is exactly the order we want.
Algorithm.
- Count occurrences.
- Create
n + 1empty lists, indexed by count0..n. - For each entry
(value, count), appendvaluetobuckets.get(count). - For
ffromndown to 1, and while fewer thankvalues have been collected, copy the values ofbuckets.get(f)into the result.
static int[] optimal(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int x : nums) freq.merge(x, 1, Integer::sum);
List<List<Integer>> buckets = new ArrayList<>(); // buckets.get(f) = values seen exactly f times
for (int f = 0; f <= nums.length; f++) buckets.add(new ArrayList<>());
for (Map.Entry<Integer, Integer> e : freq.entrySet()) buckets.get(e.getValue()).add(e.getKey());
int[] res = new int[k];
int idx = 0;
for (int f = nums.length; f >= 1 && idx < k; f--) {
for (int x : buckets.get(f)) {
if (idx == k) break;
res[idx++] = x;
}
}
return res;
}
Dry run on example 1, n = 6, counts {1: 3, 2: 2, 3: 1}:
| f | bucket | collected |
|---|---|---|
| 6, 5, 4 | empty | |
| 3 | [1] | [1] |
| 2 | [2] | [1, 2], which is k values, so stop |
Complexity. Time O(n): counting is O(n), creating the buckets is O(n), and the final walk touches each bucket and each distinct value at most once. Space O(n) for the buckets.
Another O(n)-average option is quickselect on the distinct values by count, as in “k-th largest element”. It uses less memory than the buckets, but it is harder to get right and quadratic in the worst case. The bucket approach is the one usually expected.
Edge cases and pitfalls
- Bucket size. A count can be as large as
n(for example[2, 2, 2, 2]), so the bucket list needs indices0..n, which isn + 1lists. Sizing it tonthrowsIndexOutOfBoundsExceptionon that input. - Generic arrays.
new List<Integer>[n + 1]does not compile without an unchecked cast. AList<List<Integer>>avoids the problem entirely. - Comparing counts, not values. The heap comparator must compare
freq.get(p)withfreq.get(q). Comparingpandqwould silently give theklargest values. - Negative values. Values can be negative, so they cannot index an array directly. That is why the counting uses a
HashMap. Counts are always positive, so they can index the buckets. - Ties. The problem guarantees no tie at the cut-off. Ties above the cut-off do not matter, as in
[1, 2, 1, 2, 1, 2, 3, 1, 3, 2]withk = 2, where both 1 and 2 occur 4 times and both are needed. The random tests in the Java file deliberately skip inputs with a tie at positionk, because the expected answer would not be well defined. - Order of the output is free. The tests sort both arrays before comparing them.
Related problems
- Sort Characters by Frequency is the same count-then-order-by-count pipeline on characters.
- K-th Largest element in an array is the size-k heap and quickselect ideas on plain values.
- Highest Occurring Element in an Array is the
k = 1special case. - Majority Element-I asks for a value above a fixed frequency threshold, which can be found in
O(1)space. - Kth largest element in a stream of running integers uses the same size-k min-heap on a stream.