Always know the smallest
Think of an emergency room. Patients don’t wait in arrival order: the most urgent case is always treated next. New patients arrive all the time, and each time, the staff just needs to know who is most urgent right now. They don’t need the full list in order.
A heap (in code, usually a priority queuePriority queueA collection that always hands out the smallest (or largest) element next, usually built on a heap.Open in glossary →) is built for exactly that:
- peek at the smallest element (a min-heap) or the largest (a max-heap): O(1),
- add an element: O(log n),
- remove the smallest (or largest): O(log n).
It doesn’t keep everything sorted, which is why it’s faster than sorting: it only keeps enough order to know the top.
A binary heap is a complete binary tree stored in an array, satisfying the heap property (each parent ≤ its children for a min-heap). Insert and extract are O(log n) sift-up/sift-down operations, peek is O(1), and building a heap from n elements (heapify) is O(n). The interview patterns are bounded heaps for top-k (keep k elements, O(n log k)), k-way merges (heap of the current head of each list), two heaps for running medians, and lazy deletion when elements must be removed from the middle. The Detailed version adds internals, the iteration-order trap and comparator bugs.
import HeapModule // swift-collections: the standard library has no heap
var heap = Heap<Int>()
for value in [5, 1, 8, 3] { heap.insert(value) }
print(heap.min!) // 1: peek at the smallest
print(heap.popMin()!) // 1: remove it
print(heap.popMax()!) // 8: Heap is a min-max heap, so both ends are cheapval heap = PriorityQueue<Int>() // min-heap by default
listOf(5, 1, 8, 3).forEach { heap.add(it) }
println(heap.peek()) // 1: peek at the smallest
println(heap.poll()) // 1: remove it
val maxHeap = PriorityQueue<Int>(compareByDescending { it }) // or reverseOrder()PriorityQueue<Integer> heap = new PriorityQueue<>(); // min-heap by default
for (int value : List.of(5, 1, 8, 3)) heap.add(value);
System.out.println(heap.peek()); // 1: peek at the smallest
System.out.println(heap.poll()); // 1: remove it
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());// No built-in heap: a small binary heap with a comparator (or a package)
class Heap<T> {
private readonly items: T[] = [];
constructor(private readonly before: (a: T, b: T) => boolean) {} // true if a belongs above b
get size() { return this.items.length; }
get unordered(): readonly T[] { return this.items; } // the backing array, heap order
peek(): T | undefined { return this.items[0]; }
push(value: T) {
const a = this.items;
a.push(value);
let i = a.length - 1;
while (i > 0) { // sift up
const parent = (i - 1) >> 1;
if (!this.before(a[i], a[parent])) break;
[a[i], a[parent]] = [a[parent], a[i]];
i = parent;
}
}
pop(): T | undefined {
const a = this.items;
const top = a[0];
const last = a.pop();
if (a.length > 0 && last !== undefined) {
a[0] = last;
let i = 0;
while (true) { // sift down
const l = 2 * i + 1;
const r = l + 1;
let best = i;
if (l < a.length && this.before(a[l], a[best])) best = l;
if (r < a.length && this.before(a[r], a[best])) best = r;
if (best === i) break;
[a[i], a[best]] = [a[best], a[i]];
i = best;
}
}
return top;
}
}
const heap = new Heap<number>((a, b) => a < b); // a min-heap
for (const value of [5, 1, 8, 3]) heap.push(value);
console.log(heap.peek()); // 1: peek at the smallest
console.log(heap.pop()); // 1: remove it
const maxHeap = new Heap<number>((a, b) => a > b);Each language has a different starting point:
- Kotlin and Java use
java.util.PriorityQueue, a min-heap by default. For a max-heap, pass a reversed comparator. - Swift has no heap in the standard library; Apple’s swift-collections package provides
Heap, which supports bothminandmaxefficiently. - TypeScript has no heap at all: write a small one like the class above, or use a package.
- In interviews, it’s fine to say “assume a priority queue” in Swift or TypeScript and focus on the algorithm.
How a heap stays fast
A binary heap is a tree where every parent is smaller than its children (for a min-heap), stored in a plain array: the children of index i are at 2i + 1 and 2i + 2, and its parent at (i − 1) / 2.
- Adding: put the new value at the end of the array, then swap it with its parent while it’s smaller (“sift up”). At most one swap per level: O(log n).
- Removing the minimum: take the root, move the last element to the root, then swap it down with its smaller child while it’s bigger (“sift down”): O(log n).
The tree is always as balanced as possible, so it’s about log₂(n) levels deep: 20 for a million elements.
Internals and their consequences
- Iteration order isn’t sorted: iterating or printing a
PriorityQueueshows the backing array, which satisfies only the heap property (the puzzle). To get sorted output, poll repeatedly. - Heapify is O(n): building from a collection (
PriorityQueue(collection),Heap(array)) sifts down from the middle and is linear, faster than n inserts at O(n log n). - No efficient removal of arbitrary elements:
PriorityQueue.remove(x)is O(n). Algorithms that need it (sliding window medians, cancelled timers) use lazy deletion: mark the element as deleted (a hash map of pending deletions), and discard it when it reaches the top. - Comparators define everything: they must be consistent and must not overflow (the bug below). A comparator on a mutable field breaks the heap if that field changes while the element is inside.
- Where heaps appear in production:
ScheduledThreadPoolExecutorand timer wheels, Dijkstra and A* in routing, top-k aggregation in analytics, and k-way merges in databases and log processing.
Printing a priority queue
Predict the output
3, 1 and 2 are added to a min-heap. What does printing its contents show?
var heap = Heap<Int>()
for value in [3, 1, 2] { heap.insert(value) }
print(Array(heap.unordered))val heap = PriorityQueue<Int>()
listOf(3, 1, 2).forEach { heap.add(it) }
println(heap)PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int value : List.of(3, 1, 2)) heap.add(value);
System.out.println(heap);const heap = new Heap<number>((a, b) => a < b);
for (const value of [3, 1, 2]) heap.push(value);
console.log(heap.unordered);The contents are shown in the backing array's order, which only guarantees that each parent is smaller than its children: after inserting 3, then 1 (which sifts up to the root), then 2 (placed under 1, no swap needed), the array is [1, 3, 2]. Logging a priority queue to "see the order", or iterating it to build a sorted list, gives wrong results. Swift's Heap (and the TypeScript class in this lesson) makes this explicit by calling the view unordered.
Deep dives
Deep diveWhy heapify is O(n)Interview favorite
Building a heap with n inserts costs O(n log n). Building it in place from an array, by sifting down every node from the middle back to the root, costs only O(n), which surprises people.
The reason: sift-down costs the node’s height, and most nodes are near the bottom. Half the nodes are leaves (height 0, no work), a quarter have height 1, an eighth height 2, and so on. The total is n · (0/2 + 1/4 + 2/8 + 3/16 + …), and that sum converges to n. That’s why PriorityQueue(collection) and Heap(array) are faster than adding elements one by one.
| Operation | Cost | Swift (HeapModule) | Kotlin / Java (PriorityQueue) | TypeScript (your own Heap or a package) |
|---|---|---|---|---|
| peek min / max | O(1) | min, max | peek() (min by default) | peek() (order set by the comparator) |
| insert | O(log n) | insert(_:) | add / offer | push |
| remove top | O(log n) | popMin(), popMax() | poll() | pop() |
| build from n items | O(n) | Heap(array) | PriorityQueue(collection) | heapify by hand, or n pushes (O(n log n)) |
| remove arbitrary | O(n) | — | remove(x); prefer lazy deletion | lazy deletion |
Patterns: top-k with a bounded heap of the opposite kind (min-heap for largest), k-way merge (heap of heads), two heaps for medians, scheduling by time or priority, Dijkstra. Gotchas: default is a min-heap, iteration isn’t sorted, comparators that subtract overflow, mutating a field the comparator uses, Integer sums of two tops overflow.
Check this part
What does a min-heap give you in O(1)?
Where does a Swift heap come from?What is
PriorityQueuewith no comparator?Where does a TypeScript heap come from?Printing a heap after adding 3, 1, 2 shows…
What does building a heap from n existing elements (heapify) cost?