Heap Pattern for Problem Solving
When the Answer Is "Whatever Is Best Right Now"
A heap (priority queue) gives you the smallest (or largest) item in O(1) and lets you insert or remove in O(log n). The Heap page explains how it works inside. This page is about which problems want one.
THE SIGNAL: you repeatedly need the current best / worst / earliest / closest
item, while items keep being ADDED or REMOVED.
sorted array: min in O(1), but insert costs O(n)
plain array: insert in O(1), but min costs O(n)
heap: min in O(1), insert and remove-min in O(log n) ← balanced
The Four Recurring Shapes
1. TOP K keep only the k best seen so far
→ min-heap of size k for the LARGEST k
→ max-heap of size k for the SMALLEST k
2. K-WAY MERGE combine k sorted sources in order
→ heap of each source's current front item
3. TWO HEAPS balance two halves, read the middle
→ max-heap (lower half) + min-heap (upper half)
4. EVENT SCHEDULING process by time, track what's still running
→ min-heap of end times / next free moments
Shape 1: Top k
scores = [42, 17, 88, 63, 5, 91, 30, 77], k = 3
min-heap of size 3 (root = weakest of the current top 3):
42 heap {42}
17 heap {17, 42}
88 heap {17, 42, 88}
63 push → {17, 42, 63, 88} → too big, pop 17 → {42, 63, 88}
5 push → pop 5 immediately → {42, 63, 88}
91 push → pop 42 → {63, 88, 91}
30 push → pop 30 → {63, 88, 91}
77 push → pop 63 → {77, 88, 91}
Top 3: 77, 88, 91. Time O(n log k), space O(k).
The counter-intuitive part: the k largest live in a min-heap, because what you constantly need to know is the smallest member — the one to evict.
Shape 2: k-Way Merge
lists: [1, 7, 12] [3, 4, 15] [2, 9] [5]
heap holds one front per list: {1, 3, 2, 5}
pop 1 (list 0) → push 7 {2, 3, 5, 7}
pop 2 (list 2) → push 9 {3, 5, 7, 9}
pop 3 (list 1) → push 4 {4, 5, 7, 9}
...
merged: 1 2 3 4 5 7 9 12 15
N items total, k lists → O(N log k)
Shape 3: Two Heaps for a Running Median
stream: 5, 15, 1, 3, 8, 7
LOW = max-heap (smaller half) HIGH = min-heap (larger half)
rule: every LOW value ≤ every HIGH value, and |LOW| = |HIGH| or |HIGH| + 1
add 5 LOW {5} HIGH {} median 5.0
add 15 LOW {5} HIGH {15} median (5+15)/2 = 10.0
add 1 LOW {5, 1} HIGH {15} median 5.0
add 3 LOW {3, 1} HIGH {5, 15} median (3+5)/2 = 4.0
add 8 LOW {5, 3, 1} HIGH {8, 15} median 5.0
add 7 LOW {5, 3, 1} HIGH {7, 8, 15} median (5+7)/2 = 6.0
Each insert is O(log n), and the median is read from the roots in O(1).
Shape 4: How Many Rooms?
meetings [start, end): [9,10) [9,12) [10,11) [11,13) [12,14) [10,12)
sorted by start; heap = end times of meetings in progress
[9,10) heap {10} rooms 1
[9,12) 10 > 9, can't reuse {10, 12} rooms 2
[10,11) 10 ≤ 10 → reuse (pop 10) {11, 12}
[10,12) 11 > 10, can't reuse {11, 12, 12} rooms 3
[11,13) 11 ≤ 11 → reuse {12, 12, 13}
[12,14) 12 ≤ 12 → reuse {12, 13, 14}
Answer: 3 rooms (the most ever in use at once)
Code: All Four Shapes
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.PriorityQueue;
public class HeapPatterns {
// 1. Top k largest — MIN-heap of size k; its root is the weakest of the best
static List<Integer> topK(int[] values, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int v : values) {
heap.offer(v);
if (heap.size() > k) heap.poll(); // drop the smallest of the k+1
}
List<Integer> out = new ArrayList<>(heap);
Collections.sort(out);
return out;
}
// 2. k-way merge — heap holds {value, listIndex, position} of each list's front
static List<Integer> mergeSortedLists(int[][] lists) {
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
for (int i = 0; i < lists.length; i++)
if (lists[i].length > 0) heap.offer(new int[]{lists[i][0], i, 0});
List<Integer> out = new ArrayList<>();
while (!heap.isEmpty()) {
int[] top = heap.poll();
out.add(top[0]);
int li = top[1], next = top[2] + 1;
if (next < lists[li].length) heap.offer(new int[]{lists[li][next], li, next});
}
return out;
}
// 3. Running median — max-heap for the lower half, min-heap for the upper half
static List<Double> runningMedians(int[] stream) {
PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder());
PriorityQueue<Integer> high = new PriorityQueue<>();
List<Double> medians = new ArrayList<>();
for (int x : stream) {
if (low.isEmpty() || x <= low.peek()) low.offer(x); else high.offer(x);
if (low.size() > high.size() + 1) high.offer(low.poll()); // rebalance
else if (high.size() > low.size()) low.offer(high.poll());
medians.add(low.size() > high.size() ? low.peek() : (low.peek() + high.peek()) / 2.0);
}
return medians;
}
// 4. Rooms needed — min-heap of end times of meetings in progress
static int roomsNeeded(int[][] meetings) {
int[][] sorted = meetings.clone();
Arrays.sort(sorted, (a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1]));
PriorityQueue<Integer> ends = new PriorityQueue<>();
int most = 0;
for (int[] m : sorted) {
if (!ends.isEmpty() && ends.peek() <= m[0]) ends.poll(); // that room is free again
ends.offer(m[1]);
most = Math.max(most, ends.size());
}
return most;
}
public static void main(String[] args) {
int[] scores = {42, 17, 88, 63, 5, 91, 30, 77};
System.out.println("top 3 of " + Arrays.toString(scores) + ": " + topK(scores, 3));
System.out.println("merged: " + mergeSortedLists(new int[][]{{1, 7, 12}, {3, 4, 15}, {2, 9}, {5}}));
StringBuilder sb = new StringBuilder("running medians:");
for (double m : runningMedians(new int[]{5, 15, 1, 3, 8, 7})) sb.append(String.format(" %.1f", m));
System.out.println(sb);
int[][] meetings = {{9, 10}, {9, 12}, {10, 11}, {11, 13}, {12, 14}, {10, 12}};
System.out.println("rooms needed: " + roomsNeeded(meetings));
}
}Output:
top 3 of [42, 17, 88, 63, 5, 91, 30, 77]: [77, 88, 91]
merged: [1, 2, 3, 4, 5, 7, 9, 12, 15]
running medians: 5.0 10.0 5.0 4.0 5.0 6.0
rooms needed: 3
Heap or Something Else?
| Need | Best tool |
|---|---|
| Top k once, all data available | Heap O(n log k), or quickselect O(n) average |
| Top k from a stream / huge data | Heap of size k — O(k) memory |
| k-th smallest repeatedly with inserts | Heap(s) |
| Min/max of a sliding window | Monotonic deque (O(1) amortised) — see Monotonic Stack & Queue |
| Repeatedly the next closest / cheapest node in a graph | Heap — the engine of Dijkstra |
| Everything in sorted order, once | Just sort |
| Remove arbitrary items, ordered iteration | Balanced BST / TreeMap |
How to Spot It
✓ "k largest / k smallest / k closest / k most frequent" ✓ "merge k sorted ..." ✓ "median of a stream" / "balance two halves" ✓ "at any moment, process the earliest-ending / cheapest / highest-priority item" ✓ A greedy rule that needs "the current best" again and again (joining ropes cheapest-first, scheduling by deadline, CPU task queues)
Complexity
| Shape | Time | Space |
|---|---|---|
| Top k of n | O(n log k) | O(k) |
| k-way merge of N items | O(N log k) | O(k) |
| Running median, n inserts | O(n log n), O(1) per median read | O(n) |
| Rooms for n meetings | O(n log n) | O(n) |
Common Mistakes
Using a max-heap for the k largest. It works only if you push everything (O(n) memory) and pop k times. The size-k min-heap is the streaming-friendly version.
Forgetting Python's heapq is a min-heap only. Push negated values (or (−priority, item) tuples) for max-heap behaviour.
Comparing tuples with uncomparable items. In Python, (value, list_obj) crashes on ties when lists can't be compared. Add a tie-breaker like the list index: (value, i, idx).
Not rebalancing both directions in the two-heap median — the sizes drift and the roots stop being the middle values.
Sorting meetings by end time for room counting. Rooms are assigned as meetings start, so process them by start time. Sorting by end (the right order for the greedy "most non-overlapping" problem) gives wrong room counts here.
Interview Questions
Q: Find the k most frequent words in a large list. What's your approach?
Count frequencies with a hash map, then keep a min-heap of size k ordered by (frequency, then reverse alphabetical for ties). For each word, push and pop if the heap exceeds k. Total O(n + u log k), where u is the number of distinct words. Finally, pop all and reverse to list the most frequent first.
Q: Why does the room-counting heap only need one pop per meeting?
Meetings are processed by start time. If the earliest-ending running meeting has finished, reusing its room keeps the heap size unchanged; if not, no meeting has finished and a new room is required. Other finished meetings remain in the heap, but they never cause an overcount — their rooms get reused by later meetings one at a time — so the maximum heap size still equals the true peak.
Q: How does the two-heap approach handle deleting values (e.g. a sliding-window median)?
Heaps don't support efficient arbitrary deletion, so use lazy deletion: record values to delete in a hash map, and discard them when they surface at a heap's root. Track the "logical" sizes separately for balancing. Each operation stays O(log n) amortised.
Summary
- ›Reach for a heap when you repeatedly need the current best while items come and go.
- ›Top k largest → min-heap of size k (evict the weakest).
- ›k-way merge → heap of fronts, O(N log k).
- ›Running median → two heaps, balanced within one element.
- ›Scheduling → min-heap of end times; peak heap size = resources needed.
Next, Monotonic Stack & Queue keeps values in sorted order as they arrive, answering "next greater" and "sliding window maximum" questions in O(n) total.
To keep the k LARGEST values from a stream, which heap and why?