Heaps & Priority Queues
Heaps are the elegant data structure that lets you always find the best option in O(1) and maintain order in O(log n) — the secret engine inside every efficient scheduler, shortest-path algorithm, and sorting routine.
Overview
Imagine you are managing a hospital emergency room. Patients arrive continuously with varying severity, and you must always treat the most critical patient next — regardless of arrival order. A naive approach — scanning all waiting patients every time — takes O(n) per decision. A sorted list is expensive to maintain as new patients arrive. What you need is a data structure that makes "find the most urgent" instant, while "insert a new patient" and "remove the treated patient" are fast. That data structure is the priority queue, and its canonical implementation is the heap.
The binary heap is one of the most elegant ideas in computer science. It stores n elements in a plain array yet guarantees that the minimum (or maximum) element is always at index 0, retrievable in O(1). Insertions and deletions take only O(log n). Unlike a balanced BST, the heap achieves this with no pointers, no rotations, and minimal bookkeeping — just the discipline of a single structural invariant called the heap property. The entire data structure fits in cache-friendly contiguous memory.
The heap's importance cannot be overstated. It powers Dijkstra's shortest-path algorithm (and thus every GPS and network router), Prim's MST algorithm, A search*, operating-system process schedulers, event-driven simulation, and heapsort — an in-place O(n log n) sort with guaranteed worst-case performance. Every time your OS decides which thread runs next, or your navigation app finds the shortest route, a priority queue is at the heart of it.
This module builds from first principles: we motivate the heap property, derive the array-index arithmetic, implement insert and extract-min with careful invariant reasoning, prove the O(n) build-heap bound (a surprising and beautiful result), develop heapsort, and then survey the landscape of advanced heaps — d-ary heaps, the Fibonacci heap, and why they matter for graph algorithms. By the end you will not just know how heaps work — you will understand why they are designed the way they are.
Priority queues are ubiquitous in systems that must make best-first decisions under a stream of events. Without an efficient priority queue, Dijkstra's algorithm degrades from O((V + E) log V) to O(V²), making large-scale pathfinding — in maps, routing protocols, chip layout — impractical. Operating-system schedulers rely on priority queues to select the next runnable process; real-time systems with hard deadlines depend on O(log n) insert/extract to meet timing guarantees. Heapsort, while often beaten in practice by Timsort or quicksort due to cache effects, remains important as the only comparison sort that is simultaneously in-place and O(n log n) worst-case.
Beyond immediate applications, heaps illustrate a recurring theme in algorithm design: that a carefully chosen partial order (the heap property) is far cheaper to maintain than a total order (full sorting), yet sufficient for a surprisingly wide class of problems. The Fibonacci heap — which achieves amortized O(1) decrease-key — shows that clever lazy evaluation and deferred work can dramatically reduce algorithmic complexity. Understanding heaps deepens your intuition for amortized analysis, invariant-based reasoning, and the interplay between data-structure design and algorithm performance.
Binary Min-Heap: Array-to-Tree Correspondence
A min-heap with 7 elements stored in a flat array. Each node at index i has children at 2i+1 and 2i+2. The heap property requires every parent to be less than or equal to its children — here, 1 ≤ 3,2; 3 ≤ 7,4; 2 ≤ 5,9. Siblings (e.g. 3 and 2) have no ordering constraint.
The concepts
01The Priority Queue Abstract Data Type
A priority queue is an abstract data type (ADT) that supports three core operations:
- insert(key, value) — add a new element with a given priority
- find-min() (or find-max) — return the element with the highest priority in O(1)
- extract-min() — remove and return the element with the highest priority
Optionally: decrease-key(handle, new_key) — lower the priority of an existing element (crucial for Dijkstra).
The key insight is that a priority queue does not promise full sorted order. It promises only that the best element is always accessible quickly. This weaker guarantee is exactly what graph algorithms and schedulers need, and it is what allows much faster implementations than a sorted array or balanced BST.
A naive array gives O(1) insert but O(n) extract-min. A sorted array gives O(1) extract-min but O(n) insert. A balanced BST (like a red-black tree) gives O(log n) for both, but with large constant factors and poor cache behavior due to pointer chasing. The binary heap achieves O(log n) insert and extract-min with O(1) find-min, using a plain contiguous array — no pointers, no allocations, cache-friendly.
# Priority queue interface (Python)
from heapq import heappush, heappop
pq = [] # empty min-heap
heappush(pq, (3, 'task C')) # insert priority 3
heappush(pq, (1, 'task A')) # insert priority 1
heappush(pq, (2, 'task B')) # insert priority 2
print(pq[0]) # peek min: (1, 'task A'), O(1)
pri, task = heappop(pq) # extract min: (1, 'task A'), O(log n)
02The Binary Heap: Structure and the Heap Property
A binary heap is a complete binary tree satisfying the heap property.
Complete binary tree: every level is fully filled except possibly the last, which is filled left-to-right. This shape guarantee is what lets us store the tree in a flat array without any pointers.
Min-heap property: every node's key is ≤ the keys of its children. Equivalently, every node is smaller than its entire subtree. The root is therefore always the minimum.
Max-heap property: every node's key is ≥ its children's keys.
Note carefully what the heap property does not say: there is no ordering between siblings, and no ordering between nodes at the same level that aren't in an ancestor-descendant relationship. This is a partial order, not a total order. A heap is decidedly not a sorted array.
Array representation — The magic trick: for a 1-indexed array, node at index i has:
- Left child at index 2i
- Right child at index 2i + 1
- Parent at index ⌊i/2⌋
For 0-indexed arrays (as in Python and C):
- Left child: 2i + 1
- Right child: 2i + 2
- Parent: ⌊(i − 1)/2⌋
This arithmetic replaces pointers entirely. The complete-tree shape guarantees that these indices are always valid and the array has no gaps. A heap of n elements uses exactly n array slots.
Why does this work? A complete binary tree of n nodes has height ⌊log₂ n⌋. The height bounds the cost of all heap operations: any path from root to leaf has length at most ⌊log₂ n⌋ steps.
# Heap stored as array (0-indexed)
# Tree: 1
# / \
# 3 2
# / \ /
# 7 4 5
heap = [1, 3, 2, 7, 4, 5]
# Index arithmetic (0-based)
def left(i): return 2*i + 1
def right(i): return 2*i + 2
def parent(i): return (i - 1) // 2
print(heap[left(0)]) # 3 (left child of root)
print(heap[right(0)]) # 2 (right child of root)
print(heap[parent(3)]) # 3 (parent of index 3 = node 7)
03Heapify-Up (Sift-Up): The Insert Operation
To insert a new key k into a min-heap of size n:
- Place k at position n (the next available slot at the bottom-right of the tree). The array grows by one.
- Bubble up: while k is smaller than its parent, swap k with its parent.
- Repeat until k is at the root, or k ≥ its parent.
Why does this restore the heap property? Before insertion, every node satisfied the property. Placing k at the leaf can only violate the property between k and its parent (k might be smaller). After a swap, k moves up one level; now k's new parent might be too large again, so we check again. Because the original heap was valid, every subtree not on this root-to-leaf path is unaffected. The invariant is restored locally at each step.
Complexity: the bubble-up path has length at most h = ⌊log₂(n+1)⌋, so insert is O(log n).
Analogy: think of inserting a new employee into a corporate hierarchy where everyone must be paid less than their manager. The new employee is placed at the bottom; if they are paid more than their boss, they swap roles. This continues until the pay order is restored.
def heap_insert(heap, key):
heap.append(key) # step 1: place at end
i = len(heap) - 1
_sift_up(heap, i)
def _sift_up(heap, i):
while i > 0:
p = (i - 1) // 2 # parent index
if heap[i] < heap[p]: # heap property violated
heap[i], heap[p] = heap[p], heap[i]
i = p
else:
break # heap property restored
# Example
h = [1, 3, 2, 7, 4, 5]
heap_insert(h, 0) # insert 0
print(h) # [0, 3, 1, 7, 4, 5, 2] (0 bubbled to root)
04Heapify-Down (Sift-Down): The Extract-Min Operation
Removing the minimum (the root) is the core extraction operation:
- Save the root value (the minimum).
- Move the last element (bottom-right) to the root. Shrink the array by one.
- Sift-down (heapify-down): while the new root is larger than one of its children, swap it with the smaller child.
- Repeat until the element is ≤ both children, or is a leaf.
Why the smaller child? Suppose node x has children L and R with L < R, and x > L. If we swap x with R (the larger child), then R becomes the new parent of L — but R > L, violating the heap property at that node. We must always swap with the smaller child to keep the property intact throughout the subtree.
Complexity: the sift-down path has length at most ⌊log₂ n⌋, so extract-min is O(log n).
The combined find-min is O(1) (just read index 0). The elegance is that we never actually delete from the middle of an array — we overwrite the root with the last element and sift, doing at most 2 comparisons per level.
def heap_extract_min(heap):
if not heap:
raise IndexError("heap is empty")
min_val = heap[0]
last = heap.pop() # remove last element
if heap: # if heap not now empty
heap[0] = last # place it at root
_sift_down(heap, 0)
return min_val
def _sift_down(heap, i):
n = len(heap)
while True:
smallest = i
l, r = 2*i + 1, 2*i + 2
if l < n and heap[l] < heap[smallest]:
smallest = l
if r < n and heap[r] < heap[smallest]:
smallest = r
if smallest == i:
break # heap property satisfied
heap[i], heap[smallest] = heap[smallest], heap[i]
i = smallest
# Example
h = [1, 3, 2, 7, 4, 5]
print(heap_extract_min(h)) # 1
print(h) # [2, 3, 5, 7, 4]
05Build-Heap: O(n) Construction
Given n unsorted elements, we want to build a valid heap. The naive approach — inserting each element one by one with heap_insert — takes O(n log n). But there is a beautiful O(n) algorithm.
Floyd's build-heap algorithm:
- Load all elements into an array (any order).
- Starting from the last non-leaf (index ⌊n/2⌋ − 1 in 0-based) down to index 0, call sift-down on each node.
Leaves (indices ⌊n/2⌋ through n−1) trivially satisfy the heap property. By calling sift-down bottom-up, when we process node i, both its subtrees are already valid heaps. Sift-down restores the property rooted at i. Working bottom-up ensures correctness.
Why is this O(n) and not O(n log n)? The height of a node at depth d in a complete tree of n nodes is at most h − d, where h = ⌊log₂ n⌋. Sift-down on a node costs O(height of that node). There are at most ⌈n/2^(k+1)⌉ nodes of height k. Summing over all heights:
$$\sum_{k=0}^{h} \frac{n}{2^{k+1}} \cdot k = O(n) \sum_{k=0}^{\infty} \frac{k}{2^k} = O(n) \cdot 2 = O(n)$$
The sum ∑ k/2^k = 2 (a standard geometric series derivative). Most nodes are near the leaves and have small height — the expensive sift-down calls happen at the top, but there are very few such nodes. This is a recurring and important proof technique.
Practical consequence: building a heap from a known array is linear. Python's heapq.heapify uses exactly this algorithm.
def build_heap(arr):
"""Build a min-heap in-place in O(n)."""
n = len(arr)
# Start from last non-leaf, go up to root
for i in range(n // 2 - 1, -1, -1):
_sift_down(arr, i)
return arr
# Example
data = [9, 4, 7, 1, 8, 3, 6, 2, 5]
build_heap(data)
print(data) # [1, 2, 3, 4, 8, 7, 6, 9, 5] — valid min-heap
# Verify: data[0]=1 <= data[1]=2, data[2]=3 ✓
# Python built-in (same algorithm):
import heapq
heapq.heapify(data) # O(n), in-place
06Heapsort
Heapsort is a comparison-based, in-place, O(n log n) worst-case sorting algorithm built directly on heap operations.
Algorithm:
- Build a max-heap from the input array in O(n).
- Repeatedly extract the maximum: swap the root (max) with the last element of the heap, shrink the heap by 1, and sift-down the new root.
- After n−1 extractions, the array is sorted in ascending order.
Why a max-heap for ascending sort? Each extraction places the maximum at the end of the shrinking heap region. After all extractions, the largest elements accumulate at the back of the array — exactly ascending order.
Complexity:
- Build-heap: O(n)
- n extractions, each O(log n): O(n log n)
- Total: O(n log n), in-place, no extra memory beyond O(1)
Heapsort vs. quicksort vs. mergesort:
- Heapsort is the only sort that is simultaneously in-place and O(n log n) worst-case.
- Quicksort is faster in practice (better cache behavior, smaller constants) but O(n²) worst-case without randomization.
- Mergesort is O(n log n) worst-case but requires O(n) extra space.
- Heapsort's weakness is poor cache locality: sift-down accesses elements that are far apart in memory (parent and children differ by a factor of 2), causing many cache misses.
Introspective sort (used in C++ std::sort) combines quicksort, heapsort, and insertion sort: it uses quicksort normally but switches to heapsort if recursion depth exceeds O(log n), guaranteeing O(n log n) worst-case with quicksort's average-case speed.
def heapsort(arr):
n = len(arr)
# Step 1: Build max-heap (sift-down with max-heap logic)
def sift_down_max(arr, i, size):
while True:
largest = i
l, r = 2*i + 1, 2*i + 2
if l < size and arr[l] > arr[largest]: largest = l
if r < size and arr[r] > arr[largest]: largest = r
if largest == i: break
arr[i], arr[largest] = arr[largest], arr[i]
i = largest
for i in range(n // 2 - 1, -1, -1):
sift_down_max(arr, i, n)
# Step 2: Extract max repeatedly
for end in range(n - 1, 0, -1):
arr[0], arr[end] = arr[end], arr[0] # swap max to back
sift_down_max(arr, 0, end) # re-heapify
return arr
print(heapsort([5, 3, 8, 1, 9, 2, 7])) # [1, 2, 3, 5, 7, 8, 9]
07Applications: Dijkstra's Algorithm and Scheduling
The priority queue is the algorithmic heart of a wide class of greedy and best-first algorithms.
Dijkstra's Shortest Path Dijkstra's algorithm finds shortest paths from a source vertex s in a weighted graph with non-negative edges. The priority queue holds (distance, vertex) pairs.
- Initialize dist[s] = 0, dist[v] = ∞ for all v ≠ s. Insert all vertices.
- Repeat: extract-min to get the unvisited vertex u with smallest tentative distance. For each neighbor v of u, if dist[u] + w(u,v) < dist[v], update dist[v] and decrease-key (or re-insert) v in the PQ.
With a binary heap: O((V + E) log V). With a Fibonacci heap: O(E + V log V) — faster when E >> V (dense graphs).
OS Process Scheduling Real-time operating systems maintain a priority queue of runnable threads. The scheduler calls extract-min at every context switch (O(log n)). New processes and priority changes call insert and decrease-key. The O(log n) bound ensures scheduling decisions never become the bottleneck.
Event-Driven Simulation A discrete-event simulator maintains a priority queue of events ordered by timestamp. The simulation loop extracts the earliest event, processes it (which may insert future events), and repeats. This is the foundation of network simulators, physics engines, and financial market simulators.
Median Maintenance A classic interview problem: maintain a running median over a stream of numbers. Use two heaps — a max-heap for the lower half and a min-heap for the upper half — keeping them balanced within 1 element. The median is always at the top of one heap. Both insert and find-median are O(log n).
import heapq
def dijkstra(graph, source):
"""graph: dict of {node: [(neighbor, weight), ...]}"""
dist = {v: float('inf') for v in graph}
dist[source] = 0
pq = [(0, source)] # (distance, vertex)
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]: # stale entry, skip
continue
for v, w in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
heapq.heappush(pq, (dist[v], v))
return dist
graph = {
'A': [('B', 1), ('C', 4)],
'B': [('C', 2), ('D', 5)],
'C': [('D', 1)],
'D': []
}
print(dijkstra(graph, 'A')) # {'A':0,'B':1,'C':3,'D':4}
08d-ary Heaps
A d-ary heap generalizes the binary heap: each node has at most d children instead of 2. The array indexing generalizes cleanly:
- Children of node i: di+1, di+2, ..., di+d
- Parent of node i: ⌊(i−1)/d⌋
Trade-offs:
- Sift-up (insert) becomes faster: height is log_d(n) instead of log_2(n). Fewer swaps needed.
- Sift-down (extract-min) becomes slower: at each level, you must compare d children to find the minimum, costing d comparisons per level. Total: O(d · log_d(n)).
- For Dijkstra-like algorithms where decrease-key >> extract-min (e.g., dense graphs), larger d speeds up the algorithm overall.
Cache performance: a 4-ary heap has better cache behavior than a binary heap because more children fit in a cache line. Studies show 4-ary and 8-ary heaps outperform binary heaps in practice on modern hardware.
When to use: choose d ≈ E/V for Dijkstra on a graph with E edges and V vertices — this balances the cost of extract-min and decrease-key to yield the same O(E log_{E/V} V) complexity as a Fibonacci heap for many practical inputs, without the Fibonacci heap's implementation complexity.
# d-ary heap sift-down (extract-min)
def sift_down_d(heap, i, d, n):
while True:
smallest = i
# Check all d children
for c in range(d*i + 1, min(d*i + d + 1, n)):
if heap[c] < heap[smallest]:
smallest = c
if smallest == i:
break
heap[i], heap[smallest] = heap[smallest], heap[i]
i = smallest
# For a 4-ary heap, d=4
# Height = log_4(n) ≈ 0.5 * log_2(n)
# Each sift-down level costs 4 comparisons vs 2 for binary
# Net: ~2x faster sift-up, ~2x slower sift-down
09Decrease-Key and the Lazy Deletion Pattern
Many algorithms — Dijkstra, Prim's MST, A* — need to decrease the priority of an existing element already in the heap. This is the decrease-key operation.
In a binary heap, decrease-key requires:
- Find the element (O(n) naively, or O(1) with a hash map from element to heap index).
- Lower its key.
- Sift-up, since the element is now smaller than it should be: O(log n).
Maintaining the index map adds bookkeeping complexity.
Lazy deletion (the pattern used in most practical implementations, including Python's heapq):
- Instead of true decrease-key, insert a new copy of the element with the updated (lower) priority.
- Mark the old copy as stale.
- During extract-min, skip stale entries.
This is why the Dijkstra code shown earlier checks if d > dist[u]: continue. The heap may contain multiple entries for the same vertex; only the one with the current shortest distance is processed.
Cost: the heap may have up to E entries instead of V, so complexity becomes O(E log E) = O(E log V) for Dijkstra. This is acceptable and simpler to implement. In practice, lazy deletion is almost always the right choice over maintaining explicit index maps, unless decrease-key is called far more often than extract-min.
# Lazy deletion pattern in priority queue
from heapq import heappush, heappop
def dijkstra_lazy(graph, source):
dist = {v: float('inf') for v in graph}
dist[source] = 0
pq = [(0, source)]
while pq:
d, u = heappop(pq)
# Key lazy check: skip if we have a better dist already
if d > dist[u]:
continue # stale entry, discard
for v, w in graph[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
heappush(pq, (nd, v)) # new entry, old one is now stale
return dist
10Fibonacci Heaps and Amortized Analysis
The Fibonacci heap is an advanced priority queue that achieves better amortized bounds than any binary or d-ary heap:
| Operation | Binary Heap | Fibonacci Heap |
|---|---|---|
| insert | O(log n) | O(1) amortized |
| find-min | O(1) | O(1) |
| extract-min | O(log n) | O(log n) amortized |
| decrease-key | O(log n) | O(1) amortized |
| merge | O(n) | O(1) |
The O(1) amortized decrease-key is what makes Dijkstra run in O(E + V log V) instead of O((E + V) log V) — a difference that matters for dense graphs.
Core idea: Fibonacci heaps use a forest of heap-ordered trees (not just one tree). Insert and decrease-key are lazy — they just add a new tree or cut a node, deferring cleanup. Consolidation (merging trees of equal degree) is deferred until extract-min. The name comes from the fact that a Fibonacci heap tree of degree k has at least F(k+2) nodes (a Fibonacci number), which gives the O(log n) bound on the maximum degree.
Amortized analysis: the potential function Φ = (number of trees) + 2×(number of marked nodes). Insert costs O(1) actual + increases Φ by 1. Extract-min is expensive (consolidates the forest) but reduces Φ significantly, amortizing the cost.
Practical note: Fibonacci heaps have large constant factors and complex implementations. They are rarely used in practice outside theoretical algorithm analysis. For most real applications, binary or 4-ary heaps with lazy deletion outperform them. The Fibonacci heap's theoretical importance is in proving asymptotic bounds.
# Conceptual pseudocode — Fibonacci heap operations
# (Full implementation is ~300 lines; this shows the key ideas)
# INSERT: simply add a new single-node tree to the root list
fn fib_insert(H, x):
x.degree = 0; x.mark = false; x.child = null
add x to H.root_list
if x.key < H.min.key: H.min = x
H.n += 1
# Cost: O(1) — no consolidation!
# DECREASE-KEY: cut node, cascading cut if marked
fn fib_decrease_key(H, x, k):
x.key = k
if x.parent != null and x.key < x.parent.key:
cut(H, x, x.parent) # remove x from parent
cascading_cut(H, x.parent) # propagate cuts upward
if x.key < H.min.key: H.min = x
# Cost: O(1) amortized — only O(1) cuts on average
Key insights
- ✦The heap property is a *partial* order — it only constrains parent-child relationships, not siblings. This is the key to why heaps are cheaper to maintain than sorted arrays.
- ✦The array representation of a complete binary tree (children at 2i+1 and 2i+2, parent at (i-1)//2) is a profound space-time trade-off: zero pointer overhead, perfect cache locality, at the cost of only allowing complete-tree shapes.
- ✦Build-heap is O(n) not O(n log n) because most nodes are leaves (height 0) and even nodes at height k have O(n/2^k) representatives — the expensive work is rare.
- ✦Sift-up (insert) and sift-down (extract-min) are dual operations: sift-up restores the property violated by a too-small element at the bottom; sift-down restores it for a too-large element at the top. Both traverse a root-to-leaf path: O(log n).
- ✦In heapsort, we deliberately use a max-heap to sort ascending: each extract-max places the maximum at the back of the current array, growing the sorted suffix from right to left — no extra memory needed.
- ✦Lazy deletion (insert duplicate, skip stale entries on extract) is almost always preferable to maintaining an explicit index map for decrease-key — simpler code, competitive performance.
- ✦The Fibonacci heap's O(1) amortized decrease-key is achieved by *deferring* all expensive structural work until extract-min, amortizing via a clever potential function. Deferred work is a fundamental theme in advanced data structures.
- ✦A 4-ary heap often beats a binary heap in practice: its height is half that of a binary heap, and the 4 children of a node typically fit in a single cache line.
Common pitfalls
- ⚠Confusing sift-up and sift-down: sift-up is for insert (element too small, bubbles up); sift-down is for extract-min (replacement element too large, sinks down). Mixing them breaks the heap invariant silently.
- ⚠Using 1-indexed vs 0-indexed array formulas interchangeably: the child/parent index formulas differ. Pick one convention and stick to it throughout your implementation.
- ⚠Forgetting to swap with the *smaller* child during sift-down. Swapping with the larger child creates a new violation between the swapped-in child and the remaining smaller sibling.
- ⚠Assuming a heap is a sorted array. Siblings have no ordering relationship. A common mistake is iterating over a heap and expecting sorted output — you must extract-min n times to get sorted order.
- ⚠Off-by-one in the build-heap loop: in 0-indexed arrays, the last non-leaf is at index n//2 - 1. Starting at n//2 skips one node; starting at n//2 + 1 does unnecessary work on a leaf.
- ⚠In Dijkstra with lazy deletion, omitting the staleness check `if d > dist[u]: continue` causes already-optimal vertices to be re-relaxed, leading to incorrect results or O(E²) behavior.
- ⚠Thinking extract-min is O(1) because "the minimum is at the root." Finding the minimum is O(1) (peek), but *removing* it and restoring the heap property is O(log n) due to sift-down.
- ⚠Implementing decrease-key for a binary heap without an index map — searching for the element is O(n), making it unsuitable for Dijkstra's algorithm. Always maintain a position map or use lazy deletion.
Practice
- 01medium
Implement a complete MinHeap class in Python (or C++) with: insert, extract_min, peek, build_heap (O(n)), and __len__. Do NOT use Python's heapq. Write at least 10 unit tests covering edge cases (empty heap, single element, duplicate keys, building from a large random array).
- 02medium
Prove that build_heap is O(n). Formally: let h = floor(log2 n). There are at most ceil(n/2^(k+1)) nodes of height k. The cost of sift-down on a node of height k is O(k). Compute the exact sum and show it converges to O(n). State clearly what series identity you use.
- 03hard
Implement heapsort in C (not Python) on an int array, in-place, with no extra memory. Benchmark it against qsort on arrays of size 10^3, 10^5, 10^7. Explain in a short write-up why heapsort loses to qsort despite having the same asymptotic bound.
- 04medium
Implement the k-way merge of k sorted arrays using a min-heap. Given k sorted arrays of total n elements, produce a single sorted output array. Analyze the time and space complexity. This is the algorithm used internally by external sorting and merge steps in distributed MapReduce.
- 05medium
Implement the running median problem: given a stream of integers, after each insertion output the current median. Use two heaps (a max-heap for the lower half and a min-heap for the upper half). Prove your invariant and show that find-median is O(1) and insert is O(log n).
- 06hard
Extend your MinHeap to support decrease_key(index, new_key). Maintain a separate dict mapping element identity to its current heap index, updating the map on every swap. Use this to implement Dijkstra's algorithm with true O((V+E) log V) complexity (not the lazy version). Compare its performance against the lazy version on dense random graphs.
Resources
Checkpoints — you can now…
- ✓You can draw the array representation of a binary min-heap for any small input and verify the heap property at every node without running code.
- ✓You can implement sift-up and sift-down from memory, including the correct choice of swapping with the smaller child during sift-down.
- ✓You can explain why build-heap is O(n) — not O(n log n) — by reasoning about the number of nodes at each height level and the cost of sift-down at each level.
- ✓You can implement heapsort in-place using a max-heap and explain why the array is sorted ascending after n−1 extract-max operations.
- ✓You can implement Dijkstra's shortest-path algorithm using a priority queue (either lazy deletion or with an index map) and state its time complexity for both binary and Fibonacci heap implementations.
- ✓You can explain the trade-off between sift-up cost and sift-down cost in d-ary heaps and choose an appropriate d for a given algorithm workload.
- ✓You can describe the key ideas behind the Fibonacci heap (lazy insert/decrease-key, consolidation deferred to extract-min, potential function) and state its amortized bounds, even if you cannot implement it from memory.