int[] topK(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int num : nums) {
heap.add(num);
if (heap.size() > k) heap.poll();
}
return heap.stream().mapToInt(i -> i).toArray();
} // Implement heap.Interface for IntHeap (Len, Less, Swap, Push, Pop)
func topK(nums []int, k int) []int {
h := &IntHeap{}
for _, num := range nums {
heap.Push(h, num)
if h.Len() > k { heap.Pop(h) }
}
return []int(*h)
} import heapq
def top_k(nums, k):
heap = []
for num in nums:
heapq.heappush(heap, num)
if len(heap) > k:
heapq.heappop(heap)
return list(heap) use std::collections::BinaryHeap;
use std::cmp::Reverse;
fn top_k(nums: &[i32], k: usize) -> Vec<i32> {
let mut heap = BinaryHeap::new();
for &num in nums {
heap.push(Reverse(num));
if heap.len() > k { heap.pop(); }
}
heap.into_iter().map(|Reverse(x)| x).collect()
} // O(n log k) with min-heap; O(n log n) with sort
function topK(nums: number[], k: number): number[] {
nums.sort((a, b) => a - b);
return nums.slice(-k);
} Efficiently find the k largest, smallest, or most frequent elements using heaps.
The Top K Elements pattern uses a heap (priority queue) to maintain the top k elements as you scan through data. Instead of sorting the entire array, you keep a small heap of size k, which gives you dramatic performance improvements when k is much smaller than n.
For k largest: use a min-heap of size k. The smallest element in the heap acts as the gatekeeper. For k smallest: use a max-heap of size k. The largest element in the heap acts as the gatekeeper.
As each element arrives, add it to the heap. When the heap exceeds size k, remove the minimum. After processing all elements, the heap holds exactly the k largest values.
Finding the 3 largest elements from [3, 1, 5, 12, 2, 11] using a min-heap of size 3:
Why a min-heap for k largest? The min-heap's root is the smallest element among the current top k. Any new element larger than the root belongs in the top k, so we swap it in. Any element smaller cannot be in the top k, so we skip it. The root acts as a gatekeeper.
Look for these signals in the problem statement. Any of these phrases strongly suggest the Top K Elements pattern:
Why not just sort? Sorting gives O(n log n). With a heap of size k, you get O(n log k). When k is much smaller than n (e.g., k=10 and n=1,000,000), the difference is enormous. You also do not need the full sorted order — just the top k.
The simplest application. Maintain a min-heap of size k. After processing all elements, the root of the heap is the kth largest element.
We want the 3rd largest element. Sorted: [1, 2, 3, 4, 5, 6]. Answer = 4.
// LeetCode #215 — Kth Largest Element in an Array public int findKthLargest(int[] nums, int k) { PriorityQueue<Integer> minHeap = new PriorityQueue<>(); for (int num : nums) { minHeap.offer(num); if (minHeap.size() > k) { minHeap.poll(); // evict the smallest } } return minHeap.peek(); // root = kth largest }
// LeetCode #215 — Kth Largest Element in an Array import "sort" func findKthLargest(nums []int, k int) int { // Sort descending, then return the kth element sort.Slice(nums, func(i, j int) bool { return nums[i] > nums[j] }) return nums[k-1] }
# LeetCode #215 — Kth Largest Element in an Array import heapq def find_kth_largest(nums: list[int], k: int) -> int: min_heap: list[int] = [] for num in nums: heapq.heappush(min_heap, num) if len(min_heap) > k: heapq.heappop(min_heap) # evict the smallest return min_heap[0] # root = kth largest
// LeetCode #215 — Kth Largest Element in an Array use std::collections::BinaryHeap; use std::cmp::Reverse; impl Solution { pub fn find_kth_largest(nums: Vec<i32>, k: i32) -> i32 { // Min-heap of size k using Reverse wrapper let mut min_heap: BinaryHeap<Reverse<i32>> = BinaryHeap::new(); for num in nums { min_heap.push(Reverse(num)); if min_heap.len() > k as usize { min_heap.pop(); // evict the smallest } } min_heap.peek().unwrap().0 // root = kth largest } }
// LeetCode #215 — Kth Largest Element in an Array function findKthLargest(nums: number[], k: number): number { const minHeap = new MinPriorityQueue<number>(); for (const num of nums) { minHeap.enqueue(num); if (minHeap.size() > k) { minHeap.dequeue(); // evict the smallest } } return minHeap.front().element; // root = kth largest }
Invariant: After processing each element, the heap contains the k largest elements seen so far. The root is always the kth largest. This invariant is what makes the pattern so elegant.
First count frequencies with a HashMap. Then find the k most frequent elements. Two approaches: heap-based (general) and bucket sort (O(n) optimal).
Build a frequency map, then use a min-heap of size k on (element, frequency) pairs. The heap orders by frequency, keeping the k most frequent.
// LeetCode #347 — Top K Frequent Elements (Heap) public int[] topKFrequent(int[] nums, int k) { // Step 1: Count frequencies Map<Integer, Integer> freq = new HashMap<>(); for (int n : nums) freq.merge(n, 1, Integer::sum); // Step 2: Min-heap of size k, ordered by frequency PriorityQueue<int[]> heap = new PriorityQueue<>( (a, b) -> a[1] - b[1] ); for (var entry : freq.entrySet()) { heap.offer(new int[]{entry.getKey(), entry.getValue()}); if (heap.size() > k) heap.poll(); } // Step 3: Extract results int[] result = new int[k]; for (int i = 0; i < k; i++) result[i] = heap.poll()[0]; return result; }
// LeetCode #347 — Top K Frequent Elements (Heap) import "sort" func topKFrequent(nums []int, k int) []int { // Step 1: Count frequencies freq := make(map[int]int) for _, n := range nums { freq[n]++ } // Step 2: Collect unique values and sort by frequency desc vals := make([]int, 0, len(freq)) for v := range freq { vals = append(vals, v) } sort.Slice(vals, func(i, j int) bool { return freq[vals[i]] > freq[vals[j]] }) // Step 3: Return first k elements return vals[:k] }
# LeetCode #347 — Top K Frequent Elements (Heap) import heapq from collections import Counter def top_k_frequent(nums: list[int], k: int) -> list[int]: # Step 1: Count frequencies freq = Counter(nums) # Step 2: Min-heap of size k, ordered by frequency min_heap: list[tuple[int, int]] = [] for val, count in freq.items(): heapq.heappush(min_heap, (count, val)) if len(min_heap) > k: heapq.heappop(min_heap) # Step 3: Extract results return [val for _, val in min_heap]
// LeetCode #347 — Top K Frequent Elements (Heap) use std::collections::{BinaryHeap, HashMap}; use std::cmp::Reverse; impl Solution { pub fn top_k_frequent(nums: Vec<i32>, k: i32) -> Vec<i32> { // Step 1: Count frequencies let mut freq: HashMap<i32, i32> = HashMap::new(); for n in &nums { *freq.entry(*n).or_insert(0) += 1; } // Step 2: Min-heap (by count) of size k: (Reverse(count), val) let mut heap: BinaryHeap<(Reverse<i32>, i32)> = BinaryHeap::new(); for (&val, &count) in &freq { heap.push((Reverse(count), val)); if heap.len() > k as usize { heap.pop(); // evict least frequent } } // Step 3: Extract results heap.into_iter().map(|(_, val)| val).collect() } }
// LeetCode #347 — Top K Frequent Elements (Heap) function topKFrequent(nums: number[], k: number): number[] { // Step 1: Count frequencies const freq = new Map<number, number>(); for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1); // Step 2: Min-heap of size k, ordered by frequency const heap = new MinPriorityQueue<[number, number]>({ compare: (a, b) => a[1] - b[1] }); for (const [val, count] of freq) { heap.enqueue([val, count]); if (heap.size() > k) heap.dequeue(); } // Step 3: Extract results const result: number[] = []; while (heap.size() > 0) result.push(heap.dequeue()[0]); return result; }
Use the frequency as the bucket index. Since the maximum frequency is n, create an array of n+1 buckets. Elements with the same frequency go into the same bucket. Iterate buckets from highest to lowest.
// LeetCode #347 — Top K Frequent Elements (Bucket Sort) public int[] topKFrequent(int[] nums, int k) { // Step 1: Count frequencies Map<Integer, Integer> freq = new HashMap<>(); for (int n : nums) freq.merge(n, 1, Integer::sum); // Step 2: Create buckets indexed by frequency List<Integer>[] buckets = new List[nums.length + 1]; for (var entry : freq.entrySet()) { int f = entry.getValue(); if (buckets[f] == null) buckets[f] = new ArrayList<>(); buckets[f].add(entry.getKey()); } // Step 3: Collect from highest frequency down int[] result = new int[k]; int idx = 0; for (int i = buckets.length - 1; i >= 0 && idx < k; i--) { if (buckets[i] != null) for (int val : buckets[i]) if (idx < k) result[idx++] = val; } return result; }
// LeetCode #347 — Top K Frequent Elements (Bucket Sort) func topKFrequent(nums []int, k int) []int { // Step 1: Count frequencies freq := make(map[int]int) for _, n := range nums { freq[n]++ } // Step 2: Create buckets indexed by frequency buckets := make([][]int, len(nums)+1) for val, f := range freq { buckets[f] = append(buckets[f], val) } // Step 3: Collect from highest frequency down result := make([]int, 0, k) for i := len(buckets) - 1; i >= 0 && len(result) < k; i-- { for _, val := range buckets[i] { result = append(result, val) if len(result) == k { return result } } } return result }
# LeetCode #347 — Top K Frequent Elements (Bucket Sort) from collections import Counter def top_k_frequent(nums: list[int], k: int) -> list[int]: # Step 1: Count frequencies freq = Counter(nums) # Step 2: Create buckets indexed by frequency buckets: list[list[int]] = [[] for _ in range(len(nums) + 1)] for val, f in freq.items(): buckets[f].append(val) # Step 3: Collect from highest frequency down result: list[int] = [] for i in range(len(buckets) - 1, -1, -1): for val in buckets[i]: result.append(val) if len(result) == k: return result return result
// LeetCode #347 — Top K Frequent Elements (Bucket Sort) use std::collections::HashMap; impl Solution { pub fn top_k_frequent(nums: Vec<i32>, k: i32) -> Vec<i32> { // Step 1: Count frequencies let mut freq: HashMap<i32, usize> = HashMap::new(); for n in &nums { *freq.entry(*n).or_insert(0) += 1; } // Step 2: Create buckets indexed by frequency let mut buckets: Vec<Vec<i32>> = vec![vec![]; nums.len() + 1]; for (&val, &f) in &freq { buckets[f].push(val); } // Step 3: Collect from highest frequency down let mut result: Vec<i32> = Vec::new(); for bucket in buckets.iter().rev() { for &val in bucket { result.push(val); if result.len() == k as usize { return result; } } } result } }
// LeetCode #347 — Top K Frequent Elements (Bucket Sort) function topKFrequent(nums: number[], k: number): number[] { // Step 1: Count frequencies const freq = new Map<number, number>(); for (const n of nums) freq.set(n, (freq.get(n) ?? 0) + 1); // Step 2: Create buckets indexed by frequency const buckets: number[][] = new Array(nums.length + 1); for (const [val, f] of freq) { if (!buckets[f]) buckets[f] = []; buckets[f].push(val); } // Step 3: Collect from highest frequency down const result: number[] = []; for (let i = buckets.length - 1; i >= 0 && result.length < k; i--) { if (buckets[i]) for (const val of buckets[i]) if (result.length < k) result.push(val); } return result; }
Heap vs Bucket Sort: The heap approach works for any comparable metric and gives O(n log k). Bucket sort gives O(n) but only works when the "ranking value" (frequency) has a bounded range. In interviews, knowing both approaches demonstrates depth.
The general template for Top K Elements problems. Memorize this pattern and adapt it to specific problems.
// Template: Top K Elements using Min-Heap // Finds the k largest elements (or kth largest) PriorityQueue<Integer> minHeap = new PriorityQueue<>(); for (int num : nums) { minHeap.offer(num); if (minHeap.size() > k) { minHeap.poll(); // evict smallest } } // minHeap.peek() = kth largest element // Heap contents = top k largest elements return minHeap.peek();
// Template: Top K Elements using sort.Slice // Finds the k largest elements (or kth largest) import "sort" // Sort descending to bring k largest to the front sort.Slice(nums, func(i, j int) bool { return nums[i] > nums[j] }) // nums[k-1] = kth largest element // nums[:k] = top k largest elements return nums[k-1]
# Template: Top K Elements using Min-Heap # Finds the k largest elements (or kth largest) min_heap: list[int] = [] for num in nums: heapq.heappush(min_heap, num) if len(min_heap) > k: heapq.heappop(min_heap) # evict smallest # min_heap[0] = kth largest element # Heap contents = top k largest elements return min_heap[0]
// Template: Top K Elements using Min-Heap // Finds the k largest elements (or kth largest) use std::collections::BinaryHeap; use std::cmp::Reverse; let mut min_heap: BinaryHeap<Reverse<i32>> = BinaryHeap::new(); for num in nums { min_heap.push(Reverse(num)); if min_heap.len() > k as usize { min_heap.pop(); // evict smallest } } // min_heap.peek().unwrap().0 = kth largest element // Heap contents = top k largest elements min_heap.peek().unwrap().0
// Template: Top K Elements using Min-Heap // Finds the k largest elements (or kth largest) const minHeap = new MinPriorityQueue<number>(); for (const num of nums) { minHeap.enqueue(num); if (minHeap.size() > k) { minHeap.dequeue(); // evict smallest } } // minHeap.front().element = kth largest element // Heap contents = top k largest elements return minHeap.front().element;
// For custom objects: use comparator on the ranking metric // Example: K closest points to origin PriorityQueue<int[]> maxHeap = new PriorityQueue<>( (a, b) -> dist(b) - dist(a) // max-heap by distance ); for (int[] point : points) { maxHeap.offer(point); if (maxHeap.size() > k) { maxHeap.poll(); // evict farthest } } // Heap contains k closest points
// For custom objects: sort by the ranking metric // Example: K closest points to origin import "sort" func dist(p []int) int { return p[0]*p[0] + p[1]*p[1] } // Sort ascending by distance, take first k sort.Slice(points, func(i, j int) bool { return dist(points[i]) < dist(points[j]) }) // points[:k] contains k closest points return points[:k]
# For custom objects: use comparator on the ranking metric # Example: K closest points to origin max_heap: list[tuple[int, list[int]]] = [] for point in points: # Negate distance for max-heap behavior heapq.heappush(max_heap, (-dist(point), point)) if len(max_heap) > k: heapq.heappop(max_heap) # evict farthest # Heap contains k closest points
// For custom objects: use BinaryHeap with custom ordering // Example: K closest points to origin use std::collections::BinaryHeap; use std::cmp::Reverse; fn dist(p: &[i32]) -> i32 { p[0] * p[0] + p[1] * p[1] } // Max-heap by distance: (dist, point) — evict farthest let mut max_heap: BinaryHeap<(i32, Vec<i32>)> = BinaryHeap::new(); for point in points { let d = dist(&point); max_heap.push((d, point)); if max_heap.len() > k as usize { max_heap.pop(); // evict farthest } } // Heap contains k closest points
// For custom objects: use comparator on the ranking metric // Example: K closest points to origin const maxHeap = new MaxPriorityQueue<number[]>({ compare: (a, b) => dist(b) - dist(a) // max-heap by distance }); for (const point of points) { maxHeap.enqueue(point); if (maxHeap.size() > k) { maxHeap.dequeue(); // evict farthest } } // Heap contains k closest points
These are the classic Top K Elements problems you should practice:
Study order: Start with #215 (pure heap template), then #347 (frequency + heap), then #692 (adds tie-breaking logic), and finally #373 (multi-source heap expansion).
Enter an array and a value for k. Watch the min-heap maintain size k as elements are processed one by one.
We process each of the n elements once. Each heap insertion/removal costs O(log k) since the heap never exceeds size k. Total: O(n log k). The heap stores at most k elements, so space is O(k).
When k is small: log k is nearly constant. For k=10 and n=1,000,000, the heap approach does ~3.3 million comparisons versus ~20 million for full sort. The heap approach also works on streams of data where you cannot access the full dataset at once.