Notifications

No notifications

/Phase 4

Heaps (Priority Queues)

Heaps — Priority Queue Implementation

A heap is a complete binary tree where every parent is smaller (min-heap) or larger (max-heap) than its children.

Types

  • Min-Heap: Parent ≤ Children (root = minimum)
  • Max-Heap: Parent ≥ Children (root = maximum)

Operations

OperationTimeDescription
InsertO(log n)Add at bottom, bubble UP
Extract Min/MaxO(log n)Remove root, bubble DOWN
PeekO(1)View root element
Heapify (build)O(n)Build heap from array

Array Representation

For node at index i:
  • Parent: Math.floor((i - 1) / 2)
  • Left child: 2 * i + 1
  • Right child: 2 * i + 2

Applications

  • Priority Queue: Process highest priority first
  • Heap Sort: O(n log n) in-place sorting
  • Top K Elements: Find k largest/smallest efficiently
  • Median Finding: Two heaps (max-heap + min-heap)

On this page

Detailed Theory

A heap is the data structure you reach for whenever you need "the smallest" or "the largest" *over and over* — task schedulers, Dijkstra, Huffman codes, the median of a stream, top-K processing, every priority queue you've ever used. It's the cleanest example of how a clever invariant turns a simple array into something that gives you the extreme element in O(1) and updates in O(log n).

What a Heap Actually Is

A heap is a complete binary tree stored in a flat array that maintains one rule:

VariantHeap propertyRoot contains
Min-heapparent ≤ both childrenthe minimum
Max-heapparent ≥ both childrenthe maximum

That's the whole rule. Note what it does not say: there's no ordering between siblings, no ordering across subtrees. A heap is a much weaker invariant than a BST — and that weakness is the whole point. It's cheap to maintain.

` Max-heap Min-heap 50 1 / \ / \ 30 40 3 2 / \ / / \ / 10 20 35 5 8 4 `

Daily Mental Model: Hospital Triage

Imagine a hospital queue where the most critical patient is always seen next. New patients keep arriving with different priorities. You don't need a fully sorted list — you only need "who's worst right now?" and "remove that person, give me the next worst". That's a min-heap with severity as the key.

Beginner Mistakes to Skip

1. Confusing heap with BST. A heap has no ordering between siblings — \heap[5]\ could be larger or smaller than \heap[6]\. You can't binary-search a heap. The only thing you're guaranteed is the root. 2. Off-by-one in the index math. Parent of \i\ is \(i-1)/2\ for 0-indexed arrays. If you copy a textbook formula written for 1-indexed arrays (\i/2\), everything will silently corrupt for the first few elements. 3. Forgetting to sift after replacing the root. "Pop" isn't "shift the array". You move the last element to position 0, then sift it down. People often delete from the front and end up with O(n) per pop. 4. Using \heapq\ for a max-heap directly. Python only ships a min-heap. For max behaviour, push \-x\ and negate on pop. 5. Assuming JS has one. It doesn't. You write your own or pull a library. Don't ship \array.sort()\ inside a loop and call it a "priority queue". 6. Calling buildHeap O(n log n). It's actually O(n) — see the proof below. This is one of the most common interview gotchas.

Intermediate: Array Index Math

A complete binary tree maps perfectly to an array with no wasted slots and no pointers. For node at index \i\ (0-indexed):

QuantityFormula
Parent\(i - 1) >> 1\
Left child\2*i + 1\
Right child\2*i + 2\
Is leaf?\i >= n / 2\
Last non-leaf\(n / 2) - 1\

That's it. Heaps need zero pointers and have perfect cache locality — one of the reasons they're so fast in practice.

Intermediate: Insert (Sift Up) and Pop (Sift Down)

Insert — O(log n)

`js push(val) { this.heap.push(val); // tack onto the end let i = this.heap.length - 1; while (i > 0) { const p = (i - 1) >> 1; if (this.heap[p] <= this.heap[i]) break; // min-heap: parent already smaller [this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]]; i = p; } } `

The new element bubbles up at most height = \log₂ n\ levels.

Pop — O(log n)

`js pop() { const top = this.heap[0]; const last = this.heap.pop(); if (this.heap.length) { this.heap[0] = last; // overwrite root let i = 0, n = this.heap.length; while (true) { const l = 2*i + 1, r = 2*i + 2; let smallest = i; if (l < n && this.heap[l] < this.heap[smallest]) smallest = l; if (r < n && this.heap[r] < this.heap[smallest]) smallest = r; if (smallest === i) break; [this.heap[i], this.heap[smallest]] = [this.heap[smallest], this.heap[i]]; i = smallest; } } return top; } `

Peek — return \heap[0]\. O(1).

Intermediate: buildHeap is O(n), Not O(n log n)

The naive analysis: "n inserts × O(log n) each = O(n log n)". Wrong. The smarter algorithm is bottom-up siftDown from the last non-leaf:

`js function buildHeap(a) { for (let i = (a.length >> 1) - 1; i >= 0; i--) siftDown(a, i); } `

Why it's O(n): nodes near the bottom (most of them) sift down very few levels.

Level from bottomNode countMax sifts each
0 (leaves)n/20
1n/41
2n/82

Total work = \Σ (n/2^(k+1)) · k\ for k = 0..log n. That sum converges to n. So building a heap from an unsorted array is linear time. This is the reason heap sort starts with O(n) buildHeap, not O(n log n).

Intermediate: Heap Sort

`js function heapSort(a) { buildHeap(a); // O(n) — max-heap variant for (let end = a.length - 1; end > 0; end--) { [a[0], a[end]] = [a[end], a[0]]; // move max to its final spot siftDown(a, 0, end); // re-heapify [0..end) } } `

Properties:

  • O(n log n) worst, average, and best case.
  • O(1) extra space — fully in-place.
  • Not stable (swaps shuffle equal keys around).
  • Slower in practice than quicksort due to cache behaviour, but useful when you need guaranteed worst case + O(1) memory.

Advanced: Priority Queue Patterns You'll Recognize

Dijkstra's shortest path — pull the closest unvisited node off a min-heap, relax edges. \O((V + E) log V)\ with a binary heap.

Huffman coding — repeatedly extract two minimum-frequency nodes, merge them, push back. Builds the optimal prefix code.

A\\* search — min-heap keyed on \g + h\.

OS scheduler / event simulation — min-heap of events keyed on next-fire-time.

Top-K — keep a min-heap of size K. For each new element: if it beats the heap min, replace + sift. After processing all n, the heap contains the K largest. O(n log K) instead of O(n log n) full sort.

K-way merge — push the head of each of K sorted lists into a min-heap. Pop, append to output, push that list's next element. O(n log K).

Median of a stream — two heaps:

` max-heap (lower half) min-heap (upper half) [3, 1, 2] ← median → [4, 6, 7] `

Keep their sizes within 1 of each other. Median = root of the larger (or average of both roots if tied). O(log n) insert, O(1) median.

Advanced: D-ary Heaps and Why Dijkstra Cares

A d-ary heap has up to d children per node instead of 2.

Binary heapd-ary heap
Heightlog₂ nlog_d n
Sift-upO(log₂ n)O(log_d n) — faster
Sift-downO(log₂ n)O(d · log_d n) — slower per level

Sift-up gets faster because the path is shorter. Sift-down gets slower because you compare against d children at each level. So d-ary heaps win whenever \decrease-key\ (sift-up) is more frequent than extract-min (sift-down) — exactly the case in Dijkstra and Prim. d=4 is a common practical sweet spot.

Advanced: Fibonacci Heaps

Theoretical heavyweight. Lazy structure with merge-on-extract.

OpBinaryFibonacci (amortised)
insertO(log n)O(1)
find-minO(1)O(1)
extract-minO(log n)O(log n)
decrease-keyO(log n)O(1)
mergeO(n)O(1)

Used in the textbook complexity proof of Dijkstra (\O(E + V log V)\). In practice the constants are awful and binary or d-ary heaps win on real hardware.

Advanced: Standard Library Reality Check

LanguageAPIDefault orderWorkaround for the other
Java\PriorityQueue\min\new PriorityQueue<>(Collections.reverseOrder())\
Python\heapq\minpush \-x\ and negate on pop
C++\std::priority_queue\max\std::priority_queue, greater>\
Rust\BinaryHeap\maxwrap with \Reverse(x)\
Go\container/heap\implement \Less\ yourselfflip the comparator
JS / TSnonehand-roll one or import a library

The "Python is min, C++ is max" mismatch trips up nearly every cross-language interview. Always announce out loud which one you're using.

Practice Path

1. Implement \MinHeap\ from an array with \push\ (sift-up) and \pop\ (sift-down). Verify with a known sequence. 2. Solve Top K Frequent Elements with a size-K min-heap. Compare it to the O(n) bucket-sort solution. 3. Solve Merge K Sorted Lists in O(N log K) using a min-heap keyed on node value. 4. Implement a median-from-stream class using two heaps. Test that adding then querying after each element returns the correct median.