6. Heaps & Priority Queues

How to build, max-flip, and tie-break a heap in each language — and hand-rolling one entirely from scratch in JavaScript.

A heap is the array-backed complete binary tree your language's priority queue is built on — knowing exactly which operations are a one-line library call versus which you must hand-roll (JavaScript has nothing; Go's container/heap is verbose) is the difference between a clean setup and losing the whiteboard to boilerplate. Everything below assumes you already know why a heap gives O(log n) push/pop; this is purely about wielding each language's actual API correctly and fast.

See roadmap: Heaps & Priority Queues

Language Verdict: Pros, Cons & Recommendation

Python

3/5
  • heapq.heapify gives O(n) bulk construction for free — no need to hand-roll the sift-down loop
  • heappushpop/heapreplace fuse push+pop into a single O(log n) pass when the micro-optimization matters
  • Works directly on plain lists — no import ceremony beyond import heapq, and tuples make tie-breaking trivial
  • Min-heap only — a max-heap needs negating values (or the first tuple element), an easy detail to forget to undo on pop
  • No OOP wrapper at all: heapq is bare functions over a list, so there's no .peek()/.size(), just raw h[0]/len(h)
  • No Comparator-equivalent for complex objects — non-numeric max-heaps or custom orderings need __lt__ overrides or tuple tricks

Java

5/5
  • PriorityQueue<E> is a full class with a bulk O(n) collection constructor, offer/poll/peek, and null-safe emptiness handling
  • Full Comparator support (reverseOrder(), comparingInt(), lambdas) makes max-heaps and multi-key tie-breaking a one-liner, no negation trick
  • record-based entries implementing Comparable give clean, compile-checked tie-breaking with no array-comparator boilerplate
  • PriorityBlockingQueue exists as a drop-in thread-safe variant if concurrency ever comes up
  • No fused push+pop — offer() then poll() always costs two separate O(log n) heap operations
  • Still no decreaseKey/update method — the same lazy-deletion-with-a-map workaround as Python and JS is required

Go

3/5
  • container/heap is in the stdlib — you do not have to hand-roll sift-up/sift-down from scratch like JS
  • heap.Init is O(n) heapify; Push/Pop are O(log n) once the Interface is implemented
  • You own Less, so max-heaps and tie-breaks are a comparator flip, no negation trick required
  • You must define a named type implementing Len/Less/Swap/Push/Pop — five methods of boilerplate before the first push
  • Push/Pop on heap.Interface use any and a pointer receiver; easy to implement on the value by mistake and silently drop updates
  • No generics-era convenience heap in the stdlib until you write one yourself — still far more ceremony than Python heapq or Java PriorityQueue

JavaScript

1/5
  • Full control over comparator logic means max-heaps and tie-breaking need no library workaround at all, just flip the comparison
  • Writing the class yourself means you know its exact complexity guarantees instead of trusting a library's internals
  • No heap or priority queue in the language or Node's stdlib whatsoever — you must hand-roll a MinHeap class from memory under time pressure
  • A naive from-scratch implementation that pushes elements one at a time silently degrades O(n) heapify into O(n log n)
  • Every helper method (peek, push, pop, sift-up/down) is boilerplate you must get exactly right with no compiler or stdlib safety net
Recommendation: If you can choose your language, use Java's PriorityQueue (full Comparator support) or Python's heapq. Go's container/heap works but is awkward — budget time to paste a minimal IntHeap. In JavaScript there is no built-in heap at all — budget real interview time to hand-roll a MinHeap class from memory.

Coding Mechanics, Side by Side

Building a Heap From Existing Data

Must-know

Building a heap from a full dataset upfront is asymptotically better than pushing elements one at a time — O(n) heapify vs O(n log n) for a push-loop. JavaScript has no built-in heap at all; Go has container/heap but you still type a five-method Interface. Budget interview time accordingly.

import heapq nums = [5, 1, 8, 3, 9, 2] heapq.heapify(nums) # in-place, O(n) heapq.heappush(nums, 4) # O(log n) smallest = heapq.heappop(nums) # O(log n)

heapq only provides free functions that operate on a plain list — there is no heap class. heapify is O(n) (bottom-up sift-down from the last parent), so always prefer it over n calls to heappush (O(n log n)) when the data already exists. The list mutates in place; keep a single reference to it once heapified.

The Max-Heap Trick

Must-know
import heapq max_heap = [] for x in [5, 1, 8, 3]: heapq.heappush(max_heap, -x) # negate on the way in largest = -heapq.heappop(max_heap) # negate on the way out

heapq is min-heap only; negating numeric values is the idiomatic workaround. For non-numeric items, push (-key, item) tuples, or wrap items in a small class with __lt__ reversed. The most common bug is forgetting to re-negate a value after popping or peeking.

Tuple/Pair Tie-Breaking

Must-know
import heapq counter = 0 h = [] heapq.heappush(h, (priority, counter, task)); counter += 1 heapq.heappush(h, (priority2, counter, task2)); counter += 1 _, _, next_task = heapq.heappop(h)

Tuples compare element-by-element, so if two entries share the same priority, Python falls through to comparing task directly — raising TypeError: '<' not supported between instances of ... if task isn't orderable (e.g. a dict or a custom object without __lt__). A strictly-increasing tiebreaker as the 2nd element guarantees the comparison never reaches the payload.

heappushpop / heapreplace for Efficiency

Recommended

This is a micro-optimization, not a complexity-class change — both sides of the comparison are still O(log n). Worth knowing Python has fused ops; Java/Go/JS do not.

import heapq h = [3, 5, 9] heapq.heapify(h) smallest_after_push = heapq.heappushpop(h, 4) # push then pop, one O(log n) pass evicted = heapq.heapreplace(h, 10) # pop then push, always inserts

heappushpop does one sift instead of two independent ones from a separate heappush+heappop. heapreplace always pushes the new value even if it would be the immediate result — different semantics from heappushpop, easy to mix up.

Peek Without Removing

Must-know
h = [1, 4, 7] heapq.heapify(h) smallest = h[0] # safe: the heap invariant guarantees index 0 is the min

There's no dedicated peek function — direct indexing at [0] is the documented, idiomatic way. Raises IndexError on an empty list, so guard with if h: first.

Thread-Safety & Mutation During Iteration

Optional

Not interview-critical, but a common follow-up question: none of these four approaches are safe to mutate while iterating in undefined order — only pop-based access respects the heap invariant.

h = [3, 1, 2] heapq.heapify(h) for x in h: # iterates the underlying list in array order, NOT sorted order print(x)

heapq has no concurrency story at all — it's just list operations, so the caller is fully responsible for locking in threaded code. Iterating the list directly does not yield sorted order; only repeated heappop does.

Complexity Cheat Sheet, In Code

Recommended
nums = [5, 1, 8, 3, 9, 2, 7, 4, 6, 0] heapq.heapify(nums) # O(n) -- build from existing data heapq.heappush(nums, 10) # O(log n) top = nums[0] # O(1) -- peek heapq.heappop(nums) # O(log n) # contrast: building the same heap via a push-loop is O(n log n) slow = [] for x in nums: heapq.heappush(slow, x)

The push-loop at the bottom is the mistake to avoid — it's asymptotically worse than heapify for the exact same end state. This is the single highest-leverage fact in this section under interview time pressure.

No Decrease-Key: The Lazy-Deletion Workaround

Recommended

None of these libraries expose a decrease-key operation (unlike a textbook Fibonacci heap). The standard interview workaround is lazy deletion: push a fresher entry and let stale ones get detected and skipped on pop. Same idiom in all four languages.

best = {} # node -> current best distance h = [] def push(node, dist): best[node] = dist heapq.heappush(h, (dist, node)) def pop_valid(): while h: dist, node = heapq.heappop(h) if dist == best[node]: # stale entries are simply skipped return node, dist return None

Instead of mutating an entry in place (impossible with heapq's array layout), push a new, better (dist, node) pair and treat the old one as garbage; the up-to-date best map lets you detect and skip it on pop. Costs extra heap entries bounded by the number of updates, but avoids needing an indexed/decrease-key heap entirely.

Further Reading

  • heapq — Heap Queue AlgorithmPython official docs

    The complete module API (heapify, heappush, heappushpop, heapreplace, nlargest/nsmallest) plus the notes explaining why it's a set of functions over plain lists rather than a class.

  • PriorityQueue<E>Oracle Java Docs

    The canonical Javadoc, including the explicit 'not thread-safe' warning and the stated O(log n)/O(1) operation guarantees.

  • PriorityBlockingQueue<E>Oracle Java Docs

    The thread-safe variant of PriorityQueue — worth knowing it exists even if you rarely reach for it in a single-threaded interview setting.

  • Comparator<T>Oracle Java Docs

    Covers reverseOrder(), comparingInt(), and thenComparing() — the building blocks for max-heaps and multi-key tie-breaking in a PriorityQueue.

  • The array primitives a hand-rolled JS heap is built on; worth confirming their amortized O(1) complexity before relying on it for sift-up/sift-down.

  • container/heap — heap operationsGo official docs (pkg.go.dev)

    Documents `heap.Interface` (Len/Less/Swap/Push/Pop), `Init`/`Push`/`Pop`, and the canonical `IntHeap` example this section condenses.