Coding Interview Patterns

Course Content

Coding Interview Patterns

20 sections · 146 lessons

Heaps: The Core Idea


Think of a hospital emergency room. Patients arrive all day. The nurse never sorts the whole waiting room; she only needs to know, at any moment, who is the most urgent. When that patient is taken in, she needs the next most urgent. When a new patient arrives, she needs to fit them in quickly.

A heap is that nurse. It gives you the smallest (or largest) item of a changing collection in constant time, and it absorbs a new item or removes the top one in O(log n). It does this without ever keeping the collection sorted. That is the whole trick: it does less work than sorting, just enough to know the extreme.

A heap orders the root, and nothing else13659810
9 sits above 8 quite legally: the only promise is that no child is smaller than its parent.

How to recognise it

Four signals in the problem statement point to a heap.

  1. "Top K", "the k-th largest", "the k closest", "the k most frequent". You want a handful of extremes out of many items. This is the clearest signal there is.
  2. Data arrives over time and a statistic must be ready after each arrival. "Find the median of a stream", "return the k-th largest after every insert". Sorting on every arrival is far too slow.
  3. You keep taking the current minimum or maximum from a collection that changes. Scheduling (run the task with the most work left), simulation (process the next event in time order), and Dijkstra's algorithm (visit the nearest unvisited node) all have this shape.
  4. "Merge k sorted …". You repeatedly need the smallest head among k sources.

The constraints add a second signal. If n is up to 10⁵ or 10⁶ and k is small, the problem is asking for O(n log k), which is a size-k heap. If the input is a stream with no end, you cannot sort it at all.

Here is the brute force that the heap improves on. To find the k-th largest of n numbers, sort and index:

Python
def kth_largest_by_sorting(numbers: list[int], k: int) -> int:    """Sort from largest to smallest and pick position k - 1."""    return sorted(numbers, reverse=True)[k - 1]

That is O(n log n). For a one-off query on a modest array it is a fine answer, and you should say so. It becomes the wrong answer in two situations. When k is much smaller than n, sorting arranges a million items when you needed 5: about 20 million comparisons, against about 2.3 million for a size-5 heap. And when data is a stream, you would re-sort after every arrival.

How it works

Because the tree is complete, it packs into a plain array with no gaps and no pointers. For the item at index i:

Text
left child   = 2i + 1right child  = 2i + 2parent       = (i - 1) // 2

The array [2, 7, 4, 10, 9, 8] is this tree:

Text
            2          <- index 0          /   \        7       4      <- indices 1, 2       / \     /     10   9   8        <- indices 3, 4, 5

Check it: index 1 (value 7) has children at 3 and 4, values 10 and 9, both larger. Index 2 (value 4) has one child at 5, value 8, larger. The property holds. Note that 7 sits before 4 in the array; siblings are unordered, and that is allowed.

Push (sift-up). Put the new item at the end of the array, then swap it with its parent while it is smaller. Insert 1 into [2, 7, 4, 10, 9, 8]:

StepArrayItem atParent valueSwap?
1[2,7,4,10,9,8,1]index 64yes, 1 < 4
2[2,7,1,10,9,8,4]index 22yes, 1 < 2
3[1,7,2,10,9,8,4]index 0—root, stop

Pop (sift-down). The root is the answer. Move the last item into the root's place (so the tree stays complete), then swap it with its smaller child while it is larger than that child. Pop from [1, 7, 2, 10, 9, 8, 4]: 1 is returned, 4 moves to the root, it swaps with the smaller child 2, then stops because its only child 8 is larger. Result: [2, 7, 4, 10, 9, 8].

Swapping with the smaller child is not a detail. Swap with the larger one and the new parent is bigger than its other child, which breaks the property at once.

Sift-up: insert 110978142207142103948516new item goes at the END, keeping the treecomplete109784122071121039485461 < 4, swap109784211071221039485461 < 2, swap — now at the root2 swaps. The path length isthe tree height, log₂(n).left = 2i + 1right = 2i + 2parent = (i−1) // 2Sift-down: pop the root109782440712210394851 returned; the LAST element moves into thehole10978422071421039485compare against the SMALLER child: 2 < 4,swap109784220714210394858 > 4 — heap property restored, stopleft = 2i + 1right = 2i + 2parent = (i−1) // 2Swapping with the LARGER child would restore the property against that child and break it against the other one.
The tree is a picture; the array is the actual storage — the index arithmetic is what connects them.

The invariant. Every parent beats its children. Push and pop each repair it along one path from a leaf to the root, and that path is log₂ n long. That is why both cost O(log n), and why the root is always the extreme.

Variants

Heap problems come in four arrangements. Knowing which one you are in tells you the size of the heap and what goes into it.

ArrangementWhat the heap holdsExamples
Size-k heapThe best k candidates seen so farKth Largest, Top K Frequent, K Closest Points
Full heap, pop in orderEverything still waiting to be processedTask Scheduler, Dijkstra, event simulation
One entry per sourceThe current head of each of k sorted sourcesMerge k Sorted Lists, smallest range
Two heapsThe lower half and the upper half of the dataFind Median from Data Stream, sliding-window median

The templates

Python's heapq implements a min-heap on a plain list. Everything else is built on top of that.

Python
import heapqdef top_k_largest(numbers: list[int], k: int) -> list[int]:    """Return the k largest values, in no particular order."""    heap: list[int] = []                     # min-heap, never more than k items    for value in numbers:        heapq.heappush(heap, value)        if len(heap) > k:            heapq.heappop(heap)              # evict the smallest of the k + 1    return heap

Line by line: the heap holds the best k values seen so far. Every value is pushed. If that makes k + 1 values, the smallest one leaves, because it cannot be among the k largest. At the end, the heap holds exactly the k largest, and heap[0] is the smallest of them — the k-th largest overall.

The inversion that catches everyone: top K largest uses a MIN-heap. Derive it, don't memorise it. The heap holds your k best candidates. When a new one arrives and the heap is full, you must throw out the worst candidate you hold. For "largest", the worst is the smallest, so the structure must put the smallest on top. That is a min-heap. By the same argument, top K smallest uses a max-heap.

Three more tools finish the kit:

Python
import heapq# 1. Custom priority: heapq compares tuples field by field, so put the key first.heap: list[tuple[int, int, str]] = []heapq.heappush(heap, (3, 0, "write report"))   # (priority, counter, payload)# 2. A max-heap where only a min-heap exists: negate the key.max_heap: list[int] = []heapq.heappush(max_heap, -42)largest = -max_heap[0]                         # negate again on the way out# 3. Build from a whole list in O(n), not O(n log n).data = [9, 4, 7, 1]heapq.heapify(data)                            # data[0] is now 1

The middle field counter in the tuple is a unique tie-breaker. It matters: if two entries have the same priority, Python compares the next field, and if that field is an object with no < defined, the comparison raises TypeError. For tuples you want as a max-heap, negate only the key, (-count, word), never the payload.

Other languages differ. Java's PriorityQueue is a min-heap and takes a comparator. C++'s priority_queue is a max-heap by default — the opposite of Python — and greater<> makes it a min-heap.

Complexity

What a heap is cheap and dear atCheap• Peek the extreme: O(1)• Push or pop: O(log n)• Heapify a whole list: O(n)Dear, or impossible• Find an arbitrary value: O(n)• Ask for the middle element• Iterate in order without popping
Heapify is linear because almost every node is near the bottom and sinks only a step or two.
OperationCostWhy
Peek at the extreme, heap[0]O(1)It is the root
heappush / heappopO(log n)One path from leaf to root
heapreplace / heappushpopO(log n)Pop and push in a single sift
heapify on a listO(n)See below
Find an arbitrary valueO(n)No order to search by
SpaceO(n)One array slot per item

Why heapify is O(n). It sifts down from the bottom of the tree upwards. Half the nodes are leaves and move zero levels. A quarter can move one level, an eighth two levels, and so on. The total n/4 × 1 + n/8 × 2 + n/16 × 3 + … adds up to about n. Almost every node is near the bottom and has almost nowhere to fall. Saying this in an interview is a small remark that signals depth.

The size-k template costs O(n log k) time and O(k) space. Each of the n values does at most one push and one pop on a heap of size k. With n = 1,000,000 and k = 10, that is about 3.3 million comparisons against 20 million for a full sort, and a 10-slot heap instead of a million-slot copy.

Where it goes wrong

One conceptual bug and four mechanicalConceptual• Max-heap used for the K largest• A heap where one sort would do• A heap where counting would doMechanical• Pushing without popping to rebalance• Ties break on an uncomparable item• Mutating a key already in the heap
A heap re-sorts nothing after insertion, so a mutated key keeps a place it no longer deserves.

1. A max-heap of everything for top K largest. Heapify all n values into a max-heap and pop k times. It gives the right answer in O(n + k log n) time but O(n) space, and it cannot work on a stream. The size-k min-heap is the answer interviewers want.

2. Reading the array as if it were sorted. heap[0] is the extreme. heap[1] is not the second smallest; it is one child of the root, and the second smallest may be heap[2]. Code that reads past index 0 is almost always wrong, and it passes small tests by luck.

3. Ties. A tie on the priority makes Python compare the next tuple field. If that field is a ListNode or a dict, you get TypeError, but only when values tie, so it survives small tests. Add a unique counter. And a heap is not stable: if the problem says "break ties alphabetically", put that rule into the key, such as (-count, word).

4. Changing an item after it is in the heap. Nothing re-sifts it, so the heap property silently breaks. The fix is lazy deletion: push a fresh entry with the new priority, and when you pop an entry that is out of date, skip it.

5. A heap where something simpler is better. A static array and one query: sort, or use quickselect at O(n) average. k close to n: sort. Counts over a small range: bucket sort in O(n). Say this out loud. "Sorting is O(n log n) and fine here, but if k is small or the data streams, a size-k heap is O(n log k)" — that sentence is most of the topic.

Check your understanding

0 of 3 answered

1.You need the 10 largest values from a stream of 50 million numbers. Which structure fits best?

2.The array [1, 4, 2, 9, 5, 3] is a valid min-heap. What is heap[1]?

3.Why does heapify run in O(n) rather than O(n log n)?