Chapter 19: Selection, Order Statistics and Ranking
19.04Beginner / Intermediate4 topics22 min3 interactive animations

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

Heaps and PriorityQueueHeap sortMerge sort
19.04.1

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.

MethodTimeSpaceGood when
Sort everythingO(n log n)O(1)-O(n)K close to n
Size-K heapO(n log K)O(K)K small, or streaming data
QuickselectO(n) expectedO(1)All data in memory and order not needed
Java
1static int[] topKLargest(int[] nums, int k) {
2 PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap
3 for (int x : nums) {
4 if (heap.size() < k) {
5 heap.add(x);
6 } else if (x > heap.peek()) {
7 heap.poll();
8 heap.add(x);
9 }
10 }
11 return 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 input

Your input

Try

The heap is empty.

int[] arr · length 0

Being comparedJust movedSmallest of the K (root)

stream

50
121
32
173
94
205
16
147
Setup

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.

Step 1 / 28
Java
1public static int[] topK(int[] nums, int k) {
2 PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap
3 for (int x : nums) {
4 if (heap.size() < k) {
5 heap.add(x);
6 } else if (x > heap.peek()) {
7 heap.poll();
8 heap.add(x);
9 }
10 }
11 return heap.stream().mapToInt(i -> i).toArray();
12}

Live state

0 comparisons0 heap swaps

k

3

No condition evaluated on this step
Execution trace (dry run table)Show 28 steps
StepLineStateWhat happens
1L2k=3Keep 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.
2L4x=5 heap.size=0 k=3The heap is not full yet, so add 5.
3L5x=5Add 5 at the end, then sift it up.
4L4x=12 heap.size=1 k=3The heap is not full yet, so add 12.
5L5x=12Add 12 at the end, then sift it up.
6L5child=12 parent=5Parent is not larger than child, so the heap property holds.
7L4x=3 heap.size=2 k=3The heap is not full yet, so add 3.
8L5x=3Add 3 at the end, then sift it up.
9L5child=3 parent=5A min-heap needs parent <= child, so swap them (sift up).
10L5moved=3Swapped; 3 moves up.
11L6x=17 heap.peek()=317 beats the smallest of the current top 3 (3), so replace it.
12L7x=17poll() removes the old root; add(17) lands at the root and sifts down.
13L7node=17 smallest=5A child is smaller than the node, so swap with the smaller child (sift down).
14L7Swapped; 5 moves up.
15L7node=17 smallest=17The new root is not larger than its children. Heap restored.
16L6x=9 heap.peek()=59 beats the smallest of the current top 3 (5), so replace it.
17L7x=9poll() removes the old root; add(9) lands at the root and sifts down.
18L7node=9 smallest=9The new root is not larger than its children. Heap restored.
19L6x=20 heap.peek()=920 beats the smallest of the current top 3 (9), so replace it.
20L7x=20poll() removes the old root; add(20) lands at the root and sifts down.
21L7node=20 smallest=12A child is smaller than the node, so swap with the smaller child (sift down).
22L7Swapped; 12 moves up.
23L7node=20 smallest=20The new root is not larger than its children. Heap restored.
24L6x=1 heap.peek()=121 does not beat 12, so it cannot be in the top 3. Skip it.
25L6x=14 heap.peek()=1214 beats the smallest of the current top 3 (12), so replace it.
26L7x=14poll() removes the old root; add(14) lands at the root and sifts down.
27L7node=14 smallest=14The new root is not larger than its children. Heap restored.
28L11return=[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.
19.04.2

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.

Java
1record Node(int value, int list, int index) {}
2
3static List<Integer> mergeK(List<int[]> lists) {
4 PriorityQueue<Node> heap = new PriorityQueue<>(Comparator.comparingInt(Node::value));
5 for (int i = 0; i < lists.size(); i++) {
6 if (lists.get(i).length > 0) heap.add(new Node(lists.get(i)[0], i, 0));
7 }
8 List<Integer> out = new ArrayList<>();
9 while (!heap.isEmpty()) {
10 Node n = heap.poll();
11 out.add(n.value());
12 int next = n.index() + 1;
13 if (next < lists.get(n.list()).length) {
14 heap.add(new Node(lists.get(n.list())[next], n.list(), next));
15 }
16 }
17 return out;
18}

Worst

O(N log K)

Space

O(K)

K-way merge of sorted runs

Interactive · edit the input

Your input

Try

run 0

7pos[0]
19
34

run 1

2pos[1]
11
28

run 2

5pos[2]
23
40

out[]

empty
Setup

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.

Step 1 / 38
Java
1public static int[] kWayMerge(int[][] runs) {
2 int k = runs.length, total = 0;
3 for (int[] r : runs) total += r.length;
4 int[] pos = new int[k]; // next unread index in each run
5 int[] out = new int[total];
6 for (int o = 0; o < total; o++) {
7 int best = -1;
8 for (int r = 0; r < k; r++) {
9 if (pos[r] < runs[r].length &&
10 (best == -1 || runs[r][pos[r]] < runs[best][pos[best]])) {
11 best = r;
12 }
13 }
14 out[o] = runs[best][pos[best]++];
15 }
16 return out;
17}

Live state

0 comparisons

o

0

pos[0]

0

pos[1]

0

pos[2]

0

No condition evaluated on this step
Execution trace (dry run table)Show 38 steps
StepLineStateWhat happens
1L4o=0 pos[0]=0 pos[1]=0 pos[2]=03 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.
2L11o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=0 r=0Run 0's head 7 is the first candidate.
3L10o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=0 r=1Run 1's head is smaller, so it becomes the new best.
4L10o=0 pos[0]=0 pos[1]=0 pos[2]=0 best=1 r=2Run 1's head is still the smallest.
5L14o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=1out[0] = 2, taken from run 1.
6L11o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=0Run 0's head 7 is the first candidate.
7L10o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=1Run 0's head is still the smallest.
8L10o=1 pos[0]=0 pos[1]=1 pos[2]=0 best=0 r=2Run 2's head is smaller, so it becomes the new best.
9L14o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=2out[1] = 5, taken from run 2.
10L11o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=0Run 0's head 7 is the first candidate.
11L10o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=1Run 0's head is still the smallest.
12L10o=2 pos[0]=0 pos[1]=1 pos[2]=1 best=0 r=2Run 0's head is still the smallest.
13L14o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0out[2] = 7, taken from run 0.
14L11o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0 r=0Run 0's head 19 is the first candidate.
15L10o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=0 r=1Run 1's head is smaller, so it becomes the new best.
16L10o=3 pos[0]=1 pos[1]=1 pos[2]=1 best=1 r=2Run 1's head is still the smallest.
17L14o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=1out[3] = 11, taken from run 1.
18L11o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=0Run 0's head 19 is the first candidate.
19L10o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=1Run 0's head is still the smallest.
20L10o=4 pos[0]=1 pos[1]=2 pos[2]=1 best=0 r=2Run 0's head is still the smallest.
21L14o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0out[4] = 19, taken from run 0.
22L11o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0 r=0Run 0's head 34 is the first candidate.
23L10o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=0 r=1Run 1's head is smaller, so it becomes the new best.
24L10o=5 pos[0]=2 pos[1]=2 pos[2]=1 best=1 r=2Run 2's head is smaller, so it becomes the new best.
25L14o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=2out[5] = 23, taken from run 2.
26L11o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=0 r=0Run 0's head 34 is the first candidate.
27L10o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=0 r=1Run 1's head is smaller, so it becomes the new best.
28L10o=6 pos[0]=2 pos[1]=2 pos[2]=2 best=1 r=2Run 1's head is still the smallest.
29L14o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=1out[6] = 28, taken from run 1.
30L11o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=0Run 0's head 34 is the first candidate.
31L9o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=1Run 1 is exhausted, skip it.
32L10o=7 pos[0]=2 pos[1]=3 pos[2]=2 best=0 r=2Run 0's head is still the smallest.
33L14o=8 pos[0]=3 pos[1]=3 pos[2]=2 best=0out[7] = 34, taken from run 0.
34L9o=8 pos[0]=3 pos[1]=3 pos[2]=2 r=0Run 0 is exhausted, skip it.
35L9o=8 pos[0]=3 pos[1]=3 pos[2]=2 r=1Run 1 is exhausted, skip it.
36L11o=8 pos[0]=3 pos[1]=3 pos[2]=2 best=2 r=2Run 2's head 40 is the first candidate.
37L14o=9 pos[0]=3 pos[1]=3 pos[2]=3 best=2out[8] = 40, taken from run 2.
38L16return=[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).
19.04.3

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)).

Java
1// k-th smallest in a matrix with sorted rows
2static int kthSmallest(int[][] m, int k) {
3 PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(e -> e[0])); // {value, row, col}
4 for (int r = 0; r < m.length; r++) heap.add(new int[] { m[r][0], r, 0 });
5 int value = -1;
6 while (k-- > 0) {
7 int[] top = heap.poll();
8 value = top[0];
9 if (top[2] + 1 < m[top[1]].length) heap.add(new int[] { m[top[1]][top[2] + 1], top[1], top[2] + 1 });
10 }
11 return value;
12}
19.04.4

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 input

Your input

Try

stream

50
151
12
33
84
75
96
107

low (max-heap)

empty

high (min-heap)

empty

medians so far

empty
Setup

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.

Step 1 / 37
Java
1class MedianFinder {
2 PriorityQueue<Integer> low = new PriorityQueue<>(Comparator.reverseOrder()); // max-heap
3 PriorityQueue<Integer> high = new PriorityQueue<>(); // min-heap
4
5 void add(int x) {
6 low.add(x);
7 high.add(low.poll()); // every low value <= every high value
8 if (high.size() > low.size()) {
9 low.add(high.poll()); // keep low the same size or one larger
10 }
11 }
12
13 double median() {
14 return low.size() > high.size() ? low.peek() : (low.peek() + high.peek()) / 2.0;
15 }
16}

Live state

0 inserted

low.size

0

high.size

0

No condition evaluated on this step
Execution trace (dry run table)Show 37 steps
StepLineStateWhat happens
1L1low.size=0 high.size=0Split 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.
2L6x=5 low.size=1 high.size=0low.add(5). The new value always enters the lower half first.
3L7x=5 moved=5 low.size=0 high.size=1high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value.
4L8low.size=0 high.size=1The upper half is bigger than the lower half, so rebalance.
5L9moved=5 low.size=1 high.size=0Move 5 back to the lower half.
6L14median=5 low.size=1 high.size=0Odd count: the median is the top of low, 5.
7L6x=15 low.size=2 high.size=0low.add(15). The new value always enters the lower half first.
8L7x=15 moved=15 low.size=1 high.size=1high.add(low.poll()): the largest value of the lower half (15) moves up. This guarantees every low value is <= every high value.
9L8low.size=1 high.size=1Sizes are balanced: low is equal or one larger.
10L14median=10 low.size=1 high.size=1Even count: the median is the average of the two tops, (5 + 15) / 2 = 10.
11L6x=1 low.size=2 high.size=1low.add(1). The new value always enters the lower half first.
12L7x=1 moved=5 low.size=1 high.size=2high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value.
13L8low.size=1 high.size=2The upper half is bigger than the lower half, so rebalance.
14L9moved=5 low.size=2 high.size=1Move 5 back to the lower half.
15L14median=5 low.size=2 high.size=1Odd count: the median is the top of low, 5.
16L6x=3 low.size=3 high.size=1low.add(3). The new value always enters the lower half first.
17L7x=3 moved=5 low.size=2 high.size=2high.add(low.poll()): the largest value of the lower half (5) moves up. This guarantees every low value is <= every high value.
18L8low.size=2 high.size=2Sizes are balanced: low is equal or one larger.
19L14median=4 low.size=2 high.size=2Even count: the median is the average of the two tops, (3 + 5) / 2 = 4.
20L6x=8 low.size=3 high.size=2low.add(8). The new value always enters the lower half first.
21L7x=8 moved=8 low.size=2 high.size=3high.add(low.poll()): the largest value of the lower half (8) moves up. This guarantees every low value is <= every high value.
22L8low.size=2 high.size=3The upper half is bigger than the lower half, so rebalance.
23L9moved=5 low.size=3 high.size=2Move 5 back to the lower half.
24L14median=5 low.size=3 high.size=2Odd count: the median is the top of low, 5.
25L6x=7 low.size=4 high.size=2low.add(7). The new value always enters the lower half first.
26L7x=7 moved=7 low.size=3 high.size=3high.add(low.poll()): the largest value of the lower half (7) moves up. This guarantees every low value is <= every high value.
27L8low.size=3 high.size=3Sizes are balanced: low is equal or one larger.
28L14median=6 low.size=3 high.size=3Even count: the median is the average of the two tops, (5 + 7) / 2 = 6.
29L6x=9 low.size=4 high.size=3low.add(9). The new value always enters the lower half first.
30L7x=9 moved=9 low.size=3 high.size=4high.add(low.poll()): the largest value of the lower half (9) moves up. This guarantees every low value is <= every high value.
31L8low.size=3 high.size=4The upper half is bigger than the lower half, so rebalance.
32L9moved=7 low.size=4 high.size=3Move 7 back to the lower half.
33L14median=7 low.size=4 high.size=3Odd count: the median is the top of low, 7.
34L6x=10 low.size=5 high.size=3low.add(10). The new value always enters the lower half first.
35L7x=10 moved=10 low.size=4 high.size=4high.add(low.poll()): the largest value of the lower half (10) moves up. This guarantees every low value is <= every high value.
36L8low.size=4 high.size=4Sizes are balanced: low is equal or one larger.
37L14median=7.5 low.size=4 high.size=4Even 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.