Heap selection
A heap gives you the smallest or largest of a changing collection in O(log n). That makes it the right tool for top-K queries, merging many sorted sources, and tracking a running median.
Learning goals
- Select the top K elements with a size-K heap in O(n log K)
- Merge K sorted sources with a heap of their heads
- Find the k-th element across sorted rows or streams
- Maintain a running median with two heaps
Prerequisites
Top K largest/smallest
To keep the K largest values, use a min-heap of size K. Its root is the smallest of the K largest so far, the value a newcomer must beat. For each element: if the heap has fewer than K items, add it; otherwise, if it is larger than the root, replace the root and sift down.
Each element costs at most O(log K), so the total is O(n log K) time and O(K) space. The data may be a stream you cannot store. For the K smallest, swap in a max-heap. The animation draws the heap as a tree and marks the root: the value to beat.
| Method | Time | Space | Good when |
|---|---|---|---|
| Sort everything | O(n log n) | O(1)-O(n) | K close to n |
| Size-K heap | O(n log K) | O(K) | K small, or streaming data |
| Quickselect | O(n) expected | O(1) | All data in memory and order not needed |
1static int[] topKLargest(int[] nums, int k) {2PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap3for (int x : nums) {4if (heap.size() < k) {5heap.add(x);6} else if (x > heap.peek()) {7heap.poll();8heap.add(x);9}10}11return heap.stream().sorted(Comparator.reverseOrder()).mapToInt(i -> i).toArray();12}
Worst
O(n log k)
Space
O(k)
Top K largest with a min-heap
Interactive · edit the inputThe heap is empty.
int[] arr · length 0
stream
Keep only the 3 largest values seen so far in a min-heap. Its root is the smallest of those K, so it is the one to beat.
1public static int[] topK(int[] nums, int k) {2PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap3for (int x : nums) {4if (heap.size() < k) {5heap.add(x);6} else if (x > heap.peek()) {7heap.poll();8heap.add(x);9}10}11return heap.stream().mapToInt(i -> i).toArray();12}
Live state
k
3
Execution trace (dry run table)Show 28 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=3 | Keep only the 3 largest values seen so far in a min-heap. Its root is the smallest of those K, so it is the one to beat. |
| 2 | L4 | x=5 heap.size=0 k=3 | The heap is not full yet, so add 5. |
| 3 | L5 | x=5 | Add 5 at the end, then sift it up. |
| 4 | L4 | x=12 heap.size=1 k=3 | The heap is not full yet, so add 12. |
| 5 | L5 | x=12 | Add 12 at the end, then sift it up. |
| 6 | L5 | child=12 parent=5 | Parent is not larger than child, so the heap property holds. |
| 7 | L4 | x=3 heap.size=2 k=3 | The heap is not full yet, so add 3. |
| 8 | L5 | x=3 | Add 3 at the end, then sift it up. |
| 9 | L5 | child=3 parent=5 | A min-heap needs parent <= child, so swap them (sift up). |
| 10 | L5 | moved=3 | Swapped; 3 moves up. |
| 11 | L6 | x=17 heap.peek()=3 | 17 beats the smallest of the current top 3 (3), so replace it. |
| 12 | L7 | x=17 | poll() removes the old root; add(17) lands at the root and sifts down. |
| 13 | L7 | node=17 smallest=5 | A child is smaller than the node, so swap with the smaller child (sift down). |
| 14 | L7 | Swapped; 5 moves up. | |
| 15 | L7 | node=17 smallest=17 | The new root is not larger than its children. Heap restored. |
| 16 | L6 | x=9 heap.peek()=5 | 9 beats the smallest of the current top 3 (5), so replace it. |
| 17 | L7 | x=9 | poll() removes the old root; add(9) lands at the root and sifts down. |
| 18 | L7 | node=9 smallest=9 | The new root is not larger than its children. Heap restored. |
| 19 | L6 | x=20 heap.peek()=9 | 20 beats the smallest of the current top 3 (9), so replace it. |
| 20 | L7 | x=20 | poll() removes the old root; add(20) lands at the root and sifts down. |
| 21 | L7 | node=20 smallest=12 | A child is smaller than the node, so swap with the smaller child (sift down). |
| 22 | L7 | Swapped; 12 moves up. | |
| 23 | L7 | node=20 smallest=20 | The new root is not larger than its children. Heap restored. |
| 24 | L6 | x=1 heap.peek()=12 | 1 does not beat 12, so it cannot be in the top 3. Skip it. |
| 25 | L6 | x=14 heap.peek()=12 | 14 beats the smallest of the current top 3 (12), so replace it. |
| 26 | L7 | x=14 | poll() removes the old root; add(14) lands at the root and sifts down. |
| 27 | L7 | node=14 smallest=14 | The new root is not larger than its children. Heap restored. |
| 28 | L11 | return=[20, 17, 14] | The heap holds the 3 largest values: 20, 17, 14. Cost O(n log k) time and O(k) space, with 15 comparisons here. |
K-way merge
To merge K sorted lists, keep a min-heap holding the current head of each list. Pop the smallest, output it, and push the next element from the same list. With N elements in total, each costs O(log K), giving O(N log K) overall instead of O(N·K) for scanning all heads.
The animation uses a plain scan of the K heads so that the comparisons are visible; the heap version replaces that scan with a single poll(). The same pattern merges log files, search results from shards, and the runs of an external sort.
1record Node(int value, int list, int index) {}23static List<Integer> mergeK(List<int[]> lists) {4PriorityQueue<Node> heap = new PriorityQueue<>(Comparator.comparingInt(Node::value));5for (int i = 0; i < lists.size(); i++) {6if (lists.get(i).length > 0) heap.add(new Node(lists.get(i)[0], i, 0));7}8List<Integer> out = new ArrayList<>();9while (!heap.isEmpty()) {10Node n = heap.poll();11out.add(n.value());12int next = n.index() + 1;13if (next < lists.get(n.list()).length) {14heap.add(new Node(lists.get(n.list())[next], n.list(), next));15}16}17return out;18}
Worst
O(N log K)
Space
O(K)
K-way merge of sorted runs
Interactive · edit the inputrun 0
run 1
run 2
out[]
3 sorted runs to merge. In an external sort these would be chunks that each fit in memory and were sorted separately, then written to disk.
1public static int[] kWayMerge(int[][] runs) {2int k = runs.length, total = 0;3for (int[] r : runs) total += r.length;4int[] pos = new int[k]; // next unread index in each run5int[] out = new int[total];6for (int o = 0; o < total; o++) {7int best = -1;8for (int r = 0; r < k; r++) {9if (pos[r] < runs[r].length &&10(best == -1 || runs[r][pos[r]] < runs[best][pos[best]])) {11best = r;12}13}14out[o] = runs[best][pos[best]++];15}16return out;17}
Live state
o
0
pos[0]
0
pos[1]
0
pos[2]
0
Execution trace (dry run table)Show 38 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L4 | o=0 pos[0]=0 pos[1]=0 pos[2]=0 | 3 sorted runs to merge. In an external sort these would be chunks that each fit in memory and were sorted separately, then written to disk. |
| 2 | L11 | o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=0 r=0 | Run 0's head 7 is the first candidate. |
| 3 | L10 | o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=0 r=1 | Run 1's head is smaller, so it becomes the new best. |
| 4 | L10 | o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=1 r=2 | Run 1's head is still the smallest. |
| 5 | L14 | o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=1 | out[0] = 2, taken from run 1. |
| 6 | L11 | o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=0 | Run 0's head 7 is the first candidate. |
| 7 | L10 | o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=1 | Run 0's head is still the smallest. |
| 8 | L10 | o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=2 | Run 2's head is smaller, so it becomes the new best. |
| 9 | L14 | o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=2 | out[1] = 5, taken from run 2. |
| 10 | L11 | o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=0 | Run 0's head 7 is the first candidate. |
| 11 | L10 | o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=1 | Run 0's head is still the smallest. |
| 12 | L10 | o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=2 | Run 0's head is still the smallest. |
| 13 | L14 | o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0 | out[2] = 7, taken from run 0. |
| 14 | L11 | o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0 r=0 | Run 0's head 19 is the first candidate. |
| 15 | L10 | o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0 r=1 | Run 1's head is smaller, so it becomes the new best. |
| 16 | L10 | o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=1 r=2 | Run 1's head is still the smallest. |
| 17 | L14 | o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=1 | out[3] = 11, taken from run 1. |
| 18 | L11 | o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=0 | Run 0's head 19 is the first candidate. |
| 19 | L10 | o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=1 | Run 0's head is still the smallest. |
| 20 | L10 | o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=2 | Run 0's head is still the smallest. |
| 21 | L14 | o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0 | out[4] = 19, taken from run 0. |
| 22 | L11 | o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0 r=0 | Run 0's head 34 is the first candidate. |
| 23 | L10 | o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0 r=1 | Run 1's head is smaller, so it becomes the new best. |
| 24 | L10 | o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=1 r=2 | Run 2's head is smaller, so it becomes the new best. |
| 25 | L14 | o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=2 | out[5] = 23, taken from run 2. |
| 26 | L11 | o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=0 r=0 | Run 0's head 34 is the first candidate. |
| 27 | L10 | o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=0 r=1 | Run 1's head is smaller, so it becomes the new best. |
| 28 | L10 | o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=1 r=2 | Run 1's head is still the smallest. |
| 29 | L14 | o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=1 | out[6] = 28, taken from run 1. |
| 30 | L11 | o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=0 | Run 0's head 34 is the first candidate. |
| 31 | L9 | o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=1 | Run 1 is exhausted, skip it. |
| 32 | L10 | o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=2 | Run 0's head is still the smallest. |
| 33 | L14 | o=8 pos[0]=3 pos[1]=3 pos[2]=2 best=0 | out[7] = 34, taken from run 0. |
| 34 | L9 | o=8 pos[0]=3 pos[1]=3 pos[2]=2 r=0 | Run 0 is exhausted, skip it. |
| 35 | L9 | o=8 pos[0]=3 pos[1]=3 pos[2]=2 r=1 | Run 1 is exhausted, skip it. |
| 36 | L11 | o=8 pos[0]=3 pos[1]=3 pos[2]=2 best=2 r=2 | Run 2's head 40 is the first candidate. |
| 37 | L14 | o=9 pos[0]=3 pos[1]=3 pos[2]=3 best=2 | out[8] = 40, taken from run 2. |
| 38 | L16 | return=[2, 5, 7, 11, 19, 23, 28, 34, 40] | Merged 9 values with 15 comparisons. Scanning k heads costs O(k) per value, so a min-heap of the heads makes it O(log k) and the total O(n log k). |
Kth in sorted streams
To find the k-th smallest in a matrix whose rows are sorted, or across K sorted streams, run the k-way merge but stop after k pops. You never need to look at more than the first k elements of each row, and you never need to sort the whole matrix.
Cost: O(R + k log R) for R rows, with a heap seeded with the first element of every row. If the columns are also sorted, only the first min(R, k) rows can contain one of the k smallest, so seed just those rows. For a sorted square matrix with rows and columns both sorted, binary search on the value range with a staircase count gives O(n log(max - min)).
1// k-th smallest in a matrix with sorted rows2static int kthSmallest(int[][] m, int k) {3PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(e -> e[0])); // {value, row, col}4for (int r = 0; r < m.length; r++) heap.add(new int[] { m[r][0], r, 0 });5int value = -1;6while (k-- > 0) {7int[] top = heap.poll();8value = top[0];9if (top[2] + 1 < m[top[1]].length) heap.add(new int[] { m[top[1]][top[2] + 1], top[1], top[2] + 1 });10}11return value;12}
Running median with two heaps
Keep the lower half of the numbers in a max-heap (low) and the upper half in a min-heap (high), with sizes equal or low one larger. Then the median is low.peek() when the count is odd and the average of the two tops when it is even.
To insert x: add it to low, move low's largest to high so every low value stays at most every high value, then rebalance if high is larger. Insertion costs O(log n) and the median is O(1). The animation shows both heaps with their tops first, plus the list of medians produced.
Worst
O(log n) per insert, O(1) median
Space
O(n)
Running median with two heaps
Interactive · edit the inputstream
low (max-heap)
high (min-heap)
medians so far
Split the numbers seen so far into a lower half (max-heap, top = largest of the lower half) and an upper half (min-heap, top = smallest of the upper half). The median sits at the two tops. Heaps are drawn with their top first.
1class MedianFinder {2PriorityQueue<Integer> low = new PriorityQueue<>(Comparator.reverseOrder()); // max-heap3PriorityQueue<Integer> high = new PriorityQueue<>(); // min-heap45void add(int x) {6low.add(x);7high.add(low.poll()); // every low value <= every high value8if (high.size() > low.size()) {9low.add(high.poll()); // keep low the same size or one larger10}11}1213double median() {14return low.size() > high.size() ? low.peek() : (low.peek() + high.peek()) / 2.0;15}16}
Live state
low.size
0
high.size
0
Execution trace (dry run table)Show 37 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | low.size=0 high.size=0 | Split the numbers seen so far into a lower half (max-heap, top = largest of the lower half) and an upper half (min-heap, top = smallest of the upper half). The median sits at the two tops. Heaps are drawn with their top first. |
| 2 | L6 | x=5 low.size=1 high.size=0 | low.add(5). The new value always enters the lower half first. |
| 3 | L7 | x=5 moved=5 low.size=0 high.size=1 | high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value. |
| 4 | L8 | low.size=0 high.size=1 | The upper half is bigger than the lower half, so rebalance. |
| 5 | L9 | moved=5 low.size=1 high.size=0 | Move 5 back to the lower half. |
| 6 | L14 | median=5 low.size=1 high.size=0 | Odd count: the median is the top of low, 5. |
| 7 | L6 | x=15 low.size=2 high.size=0 | low.add(15). The new value always enters the lower half first. |
| 8 | L7 | x=15 moved=15 low.size=1 high.size=1 | high.add(low.poll()): the largest value of the lower half (15) moves up. This guarantees every low value is <= every high value. |
| 9 | L8 | low.size=1 high.size=1 | Sizes are balanced: low is equal or one larger. |
| 10 | L14 | median=10 low.size=1 high.size=1 | Even count: the median is the average of the two tops, (5 + 15) / 2 = 10. |
| 11 | L6 | x=1 low.size=2 high.size=1 | low.add(1). The new value always enters the lower half first. |
| 12 | L7 | x=1 moved=5 low.size=1 high.size=2 | high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value. |
| 13 | L8 | low.size=1 high.size=2 | The upper half is bigger than the lower half, so rebalance. |
| 14 | L9 | moved=5 low.size=2 high.size=1 | Move 5 back to the lower half. |
| 15 | L14 | median=5 low.size=2 high.size=1 | Odd count: the median is the top of low, 5. |
| 16 | L6 | x=3 low.size=3 high.size=1 | low.add(3). The new value always enters the lower half first. |
| 17 | L7 | x=3 moved=5 low.size=2 high.size=2 | high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value. |
| 18 | L8 | low.size=2 high.size=2 | Sizes are balanced: low is equal or one larger. |
| 19 | L14 | median=4 low.size=2 high.size=2 | Even count: the median is the average of the two tops, (3 + 5) / 2 = 4. |
| 20 | L6 | x=8 low.size=3 high.size=2 | low.add(8). The new value always enters the lower half first. |
| 21 | L7 | x=8 moved=8 low.size=2 high.size=3 | high.add(low.poll()): the largest value of the lower half (8) moves up. This guarantees every low value is <= every high value. |
| 22 | L8 | low.size=2 high.size=3 | The upper half is bigger than the lower half, so rebalance. |
| 23 | L9 | moved=5 low.size=3 high.size=2 | Move 5 back to the lower half. |
| 24 | L14 | median=5 low.size=3 high.size=2 | Odd count: the median is the top of low, 5. |
| 25 | L6 | x=7 low.size=4 high.size=2 | low.add(7). The new value always enters the lower half first. |
| 26 | L7 | x=7 moved=7 low.size=3 high.size=3 | high.add(low.poll()): the largest value of the lower half (7) moves up. This guarantees every low value is <= every high value. |
| 27 | L8 | low.size=3 high.size=3 | Sizes are balanced: low is equal or one larger. |
| 28 | L14 | median=6 low.size=3 high.size=3 | Even count: the median is the average of the two tops, (5 + 7) / 2 = 6. |
| 29 | L6 | x=9 low.size=4 high.size=3 | low.add(9). The new value always enters the lower half first. |
| 30 | L7 | x=9 moved=9 low.size=3 high.size=4 | high.add(low.poll()): the largest value of the lower half (9) moves up. This guarantees every low value is <= every high value. |
| 31 | L8 | low.size=3 high.size=4 | The upper half is bigger than the lower half, so rebalance. |
| 32 | L9 | moved=7 low.size=4 high.size=3 | Move 7 back to the lower half. |
| 33 | L14 | median=7 low.size=4 high.size=3 | Odd count: the median is the top of low, 7. |
| 34 | L6 | x=10 low.size=5 high.size=3 | low.add(10). The new value always enters the lower half first. |
| 35 | L7 | x=10 moved=10 low.size=4 high.size=4 | high.add(low.poll()): the largest value of the lower half (10) moves up. This guarantees every low value is <= every high value. |
| 36 | L8 | low.size=4 high.size=4 | Sizes are balanced: low is equal or one larger. |
| 37 | L14 | median=7.5 low.size=4 high.size=4 | Even count: the median is the average of the two tops, (7 + 8) / 2 = 7.5. |
Practice ladder
Now try it yourself
Easy
- Kth largest element in an array using a heap
- Last stone weight
Medium
- Top K frequent elements
- Merge k sorted lists
- Find median from a data stream
Advanced
- Kth smallest in a sorted matrix in O(k log min(n, k))
- Sliding window median
- Smallest range covering elements from K lists
Key takeaways
- A size-K min-heap keeps the K largest seen so far in O(n log K) time and O(K) space.
- A heap of list heads merges K sorted sources in O(N log K).
- Two heaps, a max-heap below and a min-heap above, give a running median with O(log n) inserts.