Heaps & Priority Queues
Array representation of complete binary trees, sift-up/down, mathematical proof of O(n) bottom-up Build-Heap, and D-ary heaps.
Binary Heaps exploit complete binary tree geometry to pack a priority queue into a flat, cache-friendly array with zero pointer overhead. By restricting structural depth to and maintaining partial ordering, heaps guarantee updates and enable Floyd's bottom-up linear-time heap construction.
1. Executive Summary & Learning Objectives
#Invented by J. W. J. Williams in 1964 for Heapsort and optimized by Robert Floyd in 1964, the Binary Heap is an array-backed data structure that implements the Priority Queue Abstract Data Type (ADT). Rather than enforcing a total global ordering across all keys, a heap enforces a local partial order: every parent key dominates its children.
By the end of this chapter, you will be able to:
- Define the Shape and Heap-Order invariants and map tree parent-child relationships to 0-indexed array arithmetic.
- Implement the fundamental
siftUpandsiftDownrestoration primitives with exact index arithmetic. - Reproduce the geometric series proof demonstrating that Floyd's bottom-up
buildHeapalgorithm executes in strictly linear time. - Trace priority queue insertions, extracts, and in-place array transformations through a structured dry-run trace table.
- Compare advanced heap architectures (Binary, -ary, Binomial, and Fibonacci heaps) across amortized time complexities and real-world cache locality.
2. Heap Invariants & Array Mapping Arithmetic
#A Binary Heap is a specialized binary tree that strictly satisfies two simultaneous invariants:
| Invariant | Formal Specification | Architectural Purpose |
|---|---|---|
| 1. Shape Invariant | The tree is a Complete Binary Tree: every level is fully populated, except possibly the bottom level, which is filled strictly from left to right. | Eliminates structural holes, allowing the tree to be mapped to a flat array without null gaps. |
| 2. Heap-Order Invariant | Max-Heap: For every node , . Min-Heap: For every node , . | Guarantees that the global extremum (maximum or minimum) resides permanently at the root index . |
Pointerless Contiguous Array Storage
#Because of the complete binary tree shape invariant, child and parent references are calculated arithmetically without storing explicit left, right, or parent pointers:
| Node Index | Formula (0-Indexed) | Formula (1-Indexed) | Boundary Condition Check |
|---|---|---|---|
| Parent Node | Valid for all (Root at index 0 has no parent) | ||
| Left Child | Valid if | ||
| Right Child | Valid if | ||
| First Internal Node | Nodes from to are guaranteed leaves |
Sample Array-Tree Correspondence
#Consider a Max-Heap containing elements: [90, 80, 70, 30, 40, 50, 10]:
| Array Index | Element Value | Logical Tree Level | Left Child (Index / Val) | Right Child (Index / Val) | Parent (Index / Val) | Node Role |
|---|---|---|---|---|---|---|
| 0 | 90 | Level 0 | Index 1 (80) | Index 2 (70) | null | Root (Global Maximum) |
| 1 | 80 | Level 1 | Index 3 (30) | Index 4 (40) | Index 0 (90) | Internal Node |
| 2 | 70 | Level 1 | Index 5 (50) | Index 6 (10) | Index 0 (90) | Internal Node |
| 3 | 30 | Level 2 | null | null | Index 1 (80) | Leaf Node |
| 4 | 40 | Level 2 | null | null | Index 1 (80) | Leaf Node |
| 5 | 50 | Level 2 | null | null | Index 2 (70) | Leaf Node |
| 6 | 10 | Level 2 | null | null | Index 2 (70) | Leaf Node |
3. Core Heap Restoration Primitives: Sift-Up & Sift-Down
#Sift-Up (heapifyUp) — Insertion ()
#When inserting an element, append it to the end of the array (at index ). If it violates the heap invariant with its parent, swap it with its parent and propagate upward:
function siftUp<T>(arr: T[], index: number): void {
let curr = index;
while (curr > 0) {
const parent = Math.floor((curr - 1) / 2);
if (arr[curr] > arr[parent]) {
// Swap with parent in Max-Heap
[arr[curr], arr[parent]] = [arr[parent], arr[curr]];
curr = parent;
} else {
break;
}
}
}Sift-Down (heapifyDown) — Extraction ()
#When extracting the root, replace arr[0] with the last element (arr[n-1]), shrink the array size, and sift the new root down by swapping it with its dominant child until the invariant is restored:
function siftDown<T>(arr: T[], n: number, index: number): void {
let curr = index;
while (true) {
let largest = curr;
const left = 2 * curr + 1;
const right = 2 * curr + 2;
if (left < n && arr[left] > arr[largest]) {
largest = left;
}
if (right < n && arr[right] > arr[largest]) {
largest = right;
}
if (largest !== curr) {
[arr[curr], arr[largest]] = [arr[largest], arr[curr]];
curr = largest;
} else {
break;
}
}
}4. Mathematical Proof: Floyd's Linear-Time Build-Heap
#A naive approach to building a heap from an unsorted array of size inserts elements one by one via siftUp, consuming time.
Floyd's Algorithm (1964) instead processes the array bottom-up, calling siftDown starting from the first non-leaf node () down to index :
export function buildHeap<T>(arr: T[]): void {
const n = arr.length;
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
siftDown(arr, n, i);
}
}Formal Mathematical Proof
#In an -element complete binary tree of height :
- At height (where leaves have height and the root has height ), there are at most nodes.
- A node at height can sift down at most levels.
- Total comparisons and swaps across all nodes is bounded by:
Let
Multiplying by :
Subtracting the two infinite series:
Substituting back into our summation:
5. Complete Implementation: Priority Queue ADT
#export class MaxPriorityQueue<T> {
private heap: T[] = [];
constructor(initialItems?: T[]) {
if (initialItems && initialItems.length > 0) {
this.heap = [...initialItems];
buildHeap(this.heap);
}
}
public get size(): number {
return this.heap.length;
}
public isEmpty(): boolean {
return this.heap.length === 0;
}
public peek(): T {
if (this.isEmpty()) throw new Error("PriorityQueue Underflow");
return this.heap[0];
}
public insert(value: T): void {
this.heap.push(value);
siftUp(this.heap, this.heap.length - 1);
}
public extractMax(): T {
if (this.isEmpty()) throw new Error("PriorityQueue Underflow");
const maxVal = this.heap[0];
const last = this.heap.pop()!;
if (this.heap.length > 0) {
this.heap[0] = last;
siftDown(this.heap, this.heap.length, 0);
}
return maxVal;
}
}6. Step-by-Step Dry Run State Trace Table
#Consider executing Floyd's buildHeap on the unsorted input array: [4, 10, 3, 5, 1], with .
First non-leaf node: . Iteration runs from down to .
| Step | Subtree Root Index | Subtree Root Val | Children () | Condition Check | Swap Action | Array State After Step |
|---|---|---|---|---|---|---|
| 1 | Left: arr[3] = 5Right: arr[4] = 1 | . Invariant holds. | No swap. | [4, 10, 3, 5, 1] | ||
| 2 | Left: arr[1] = 10Right: arr[2] = 3 | at index . Violation! | Swap arr[0] () with arr[1] (). | [10, 4, 3, 5, 1] | ||
| 3 | (sift-down) | Left: arr[3] = 5Right: arr[4] = 1 | at index . Violation! | Swap arr[1] () with arr[3] (). | [10, 5, 3, 4, 1] | |
| 4 | (leaf) | No children () | Leaf reached. Algorithm terminates. | None. | [10, 5, 3, 4, 1] (Valid Max-Heap!) |
7. Comparative Analysis: Advanced Heap Architectures
#| Heap Architecture | Find-Min/Max | Insert | Extract-Min/Max | Decrease-Key | Merge / Meld | Primary Real-World Application |
|---|---|---|---|---|---|---|
| Binary Heap | General Priority Queues, Heapsort, Event Schedulers | |||||
| -ary Heap () | Graph shortest paths; optimized for L1/L2 CPU cache lines | |||||
| Binomial Heap | amortized | Meldable priority queues, functional programming | ||||
| Fibonacci Heap | amortized | amortized | amortized | worst-case | Theoretical speedup for Dijkstra () & Prim's MST |
8. Common Traps, Edge Cases & Implementation Pitfalls
#- Off-by-One in Child Calculations:
- For 0-indexed arrays, using and leaves the root at index with identical children (). Always use and .
- Comparing Against Out-of-Bounds Children:
- In
siftDown, failing to verifyleft < nandright < ncauses undefined array index reads or premature loop termination.
- In
- Decrease-Key Without Index Tracking:
- To execute
decreaseKeyin time, the priority queue must maintain an auxiliary inverted index map (Map<Key, ArrayIndex>). Without this, finding the node requires an linear scan.
- To execute
9. Real-World Applications & Practice Problems
#Production Systems
#- Operating System Task Schedulers: Process runqueues use priority queues to schedule CPU slices based on nice values or deadlines.
- Dijkstra's & Prim's Graph Algorithms: Utilize min-priority queues to repeatedly extract the next closest unvisited vertex in time.
- Top- Streaming Systems: A min-heap of fixed size computes the rolling largest items across massive data streams in time and auxiliary memory.
Standard Practice Problems
#- Kth Largest Element in an Array (LeetCode 215) — Min-heap of size or Quickselect.
- Merge k Sorted Lists (LeetCode 23) — Min-heap tracking the head pointer of each list in time.
- Find Median from Data Stream (LeetCode 295) — Dual-heap architecture (Max-Heap for lower half, Min-Heap for upper half).
10. References & Academic Attribution
#- Williams, J. W. J. (1964). Algorithm 232: Heapsort. Communications of the ACM, 7(6), 347–348.
- Floyd, R. W. (1964). Algorithm 245: Treesort 3. Communications of the ACM, 7(12), 701.
- Fredman, M. L., & Tarjan, R. E. (1987). Fibonacci heaps and their uses in improved network optimization algorithms. Journal of the ACM (JACM), 34(3), 596–615.
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2022). Introduction to Algorithms (4th ed.), Chapter 6 (Heapsort) & Chapter 19 (Fibonacci Heaps). MIT Press.