Coding · Heap (priority queue)
The k-th largest value
Given an array nums and an integer k, return the k-th largest
value in the array. This is the k-th largest in sorted order, counting repeats: in
[7, 7, 7] the second largest is 7.
Examples
| Input | Output | Why |
|---|---|---|
nums = [3, 2, 1, 5, 6, 4], k = 2 | 5 | Sorted descending: 6, 5, 4, … |
nums = [3, 2, 3, 1, 2, 4, 5, 5, 6], k = 4 | 4 | Repeats count: 6, 5, 5, 4. |
nums = [-1, -5, -3], k = 1 | -1 | Negative values need no special case. |
Constraints
- 1 ≤ k ≤ n ≤ 10⁵
- −10⁴ ≤ nums[i] ≤ 10⁴
Hints
Hint 1
Sorting works in O(n log n). What would you keep if you could only remember k numbers while reading the array once?
Hint 2
Keep the k largest values seen so far. Of those k, which one do you need to compare each new value against?
Hint 3
Store them in a min-heap: its top is the weakest of the k. A new value bigger than the top replaces it. At the end the top is the k-th largest.
Dry run on [9,1,8,2,7,3],3
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 6nums90i1182237435heap (sorted view)90top9
9: the heap has room (1 of 3), so it goes in.
-
Step 2 / 6nums9011i82237435heap (sorted view)1091top1
1: the heap has room (2 of 3), so it goes in.
-
Step 3 / 6nums901182i237435heap (sorted view)108192top1
8: the heap has room (3 of 3), so it goes in.
-
Step 4 / 6nums90118223i7435heap (sorted view)208192top2
2 beats the heap's smallest, 1. Evict 1, insert 2.
-
Step 5 / 6nums9011822374i35heap (sorted view)708192top7
7 beats the heap's smallest, 2. Evict 2, insert 7.
-
Step 6 / 6nums901182237435iheap (sorted view)708192top7
3 is no bigger than the heap's smallest, 7, so it cannot be among the 3 largest. Skip it. Done: the top of the heap, 7, is the 3rd largest.
How to think about it
Sorting the whole array answers the question but does far more work than needed: it orders every element, when you care only about the k largest.
Read the array once and keep the k largest values seen so far. The only member of that group a newcomer needs to beat is its smallest. A min-heap gives exactly that at the top:
- While the heap holds fewer than k values, push each one.
- After that, a value larger than the top evicts it (pop, then push); a smaller or equal value cannot be among the k largest and is ignored.
When the scan ends, the heap holds the k largest values and its top, the smallest of them, is the k-th largest. Each step costs O(log k), so the total is O(n log k) time and O(k) memory.
The counter-intuitive part is using a min-heap to find a large value. The heap does not hold candidates for the answer; it holds the group the answer belongs to, and the answer is that group's weakest member.
Complexity. Time O(n log k) · Space O(k)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
import heapq
def kth_largest(nums, k):
heap = [] # the k largest so far; heap[0] is the weakest of them
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x) # pop the weakest and push x in one O(log k) step
return heap[0]#include <vector>
#include <queue>
#include <functional>
int solve(const std::vector<int>& nums, int k) {
// std::greater makes priority_queue a min-heap: top() is the weakest of the k largest
std::priority_queue<int, std::vector<int>, std::greater<int>> heap;
for (int x : nums) {
if (static_cast<int>(heap.size()) < k) heap.push(x);
else if (x > heap.top()) { heap.pop(); heap.push(x); }
}
return heap.top();
}class Solution {
public int kthLargest(int[] nums, int k) {
java.util.PriorityQueue<Integer> heap = new java.util.PriorityQueue<>(); // min-heap
for (int x : nums) {
if (heap.size() < k) heap.add(x);
else if (x > heap.peek()) { heap.poll(); heap.add(x); }
}
return heap.peek();
}
}function kthLargest(nums, k) {
const h = []; // binary min-heap of the k largest so far
const up = i => {
while (i > 0) { const p = (i - 1) >> 1; if (h[p] <= h[i]) break; [h[p], h[i]] = [h[i], h[p]]; i = p; }
};
const down = i => {
for (;;) {
const l = 2 * i + 1, r = l + 1; let m = i;
if (l < h.length && h[l] < h[m]) m = l;
if (r < h.length && h[r] < h[m]) m = r;
if (m === i) return;
[h[m], h[i]] = [h[i], h[m]]; i = m;
}
};
for (const x of nums) {
if (h.length < k) { h.push(x); up(h.length - 1); }
else if (x > h[0]) { h[0] = x; down(0); } // replace the weakest in place
}
return h[0];
}use std::cmp::Reverse;
use std::collections::BinaryHeap;
fn solve(nums: &[i64], k: usize) -> i64 {
// BinaryHeap is a max-heap; Reverse makes it a min-heap of the k largest
let mut heap = BinaryHeap::with_capacity(k + 1);
for &x in nums {
if heap.len() < k {
heap.push(Reverse(x));
} else if x > heap.peek().unwrap().0 {
heap.pop();
heap.push(Reverse(x));
}
}
heap.peek().unwrap().0
}import qualified Data.Map.Strict as M
-- A Map from value to count is a min-heap: findMin is the weakest of the k largest.
solve :: [Int] -> Int -> Int
solve nums k = fst (M.findMin heap)
where
(heap, _) = foldl step (M.empty, 0 :: Int) nums
step (m, size) x
| size < k = (M.insertWith (+) x 1 m, size + 1)
| x > fst (M.findMin m) = (M.insertWith (+) x 1 (dropMin m), size)
| otherwise = (m, size)
dropMin m = case M.findMin m of
(v, 1) -> M.delete v m
(v, c) -> M.insert v (c - 1) m
Where people lose marks
- Using a max-heap of all n values and popping k times. Correct, but O(n + k log n) time and O(n) memory, and it misses the point of the question.
- Treating the k-th largest as the k-th distinct value. With repeats, [5, 5, 4] has second largest 5.
- Replacing the top when the new value is equal to it. It does no harm to the answer, but it is wasted work; use a strict comparison.
- In Rust and C++, forgetting that the standard heap is a max-heap. Wrap values in Reverse, or use std::greater.
Variants to try
- The values arrive as a stream and the k-th largest is needed after every arrival: keep the same heap and read its top each time.
- Expected O(n) time: quickselect, partitioning around a random pivot as in quicksort but recursing into one side only. Worst case O(n²).
- The k-th smallest: the mirror image, with a max-heap of size k.
- k is close to n: keep the n − k + 1 smallest in a max-heap instead, whichever is smaller.
Next problems
Cheapest way to join ropes
Joining two ropes costs their combined length. Always join the two shortest, and a heap keeps finding them.
Open the problem → Hard25 minMost capital from at most k projects
Projects unlock as your capital grows. A sort finds what just became affordable; a heap picks the most profitable.
Open the problem →Write a solution and run it against the real test table.
Keep going
Cheapest way to join ropes
Joining two ropes costs their combined length. Always join the two shortest, and a heap keeps finding them.
Open the problem → Hard25 minMost capital from at most k projects
Projects unlock as your capital grows. A sort finds what just became affordable; a heap picks the most profitable.
Open the problem → Heap (priority queue)The pattern behind it
Keep the few elements that matter in a structure that hands you the smallest (or largest) in O(1) and updates in O(log n).
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.