Loading learning content...
We've established the problem: priority queues require O(log n) for both insert and extract, and naive linear structures can't deliver. We've hinted at the solution: a tree structure with partial ordering.
Now we meet the binary heap—one of computer science's most elegant data structures.
The heap is simultaneously:
By the end of this page, you'll understand the heap so deeply that you could implement one from scratch without reference material—and more importantly, you'll understand why it works, not just how.
By the end of this page, you will understand the two defining properties of a binary heap (structure and ordering), why these properties together enable efficient priority queue operations, how a heap visually looks as a tree, and the intuition for why operations achieve O(log n) complexity.
A binary heap is a binary tree with exactly two properties:
Property 1: Shape Property (Complete Binary Tree)
The tree is a complete binary tree: all levels are fully filled except possibly the last, which is filled left-to-right.
Property 2: Heap Property (Ordering)
For a min-heap: Every parent is ≤ its children. For a max-heap: Every parent is ≥ its children.
That's it. These two properties—one about shape, one about ordering—completely define a binary heap.
12345678910111213141516171819202122232425262728293031323334353637383940414243
# Binary Heap Properties ## Property 1: Complete Binary Tree Structure┌─────────────────────────────────────────────────────┐│ ││ Valid Complete Binary Tree: ││ ││ 1 Level 0: 1 node (full) ││ / \ ││ 2 3 Level 1: 2 nodes (full) ││ / \ / ││ 4 5 6 Level 2: filled left-to-right ││ ││ Invalid (not complete): ││ ││ 1 ││ / \ ││ 2 3 ✗ Level 2 has gaps ││ \ / ││ 5 6 ││ │└─────────────────────────────────────────────────────┘ ## Property 2: Heap Ordering (Min-Heap Example)┌─────────────────────────────────────────────────────┐│ ││ Valid Min-Heap: Every parent ≤ children ││ ││ 1 1 ≤ 2, 1 ≤ 3 ✓ ││ / \ ││ 2 3 2 ≤ 4, 2 ≤ 5, 3 ≤ 6 ✓ ││ / \ / ││ 4 5 6 ││ ││ Invalid Min-Heap: ││ ││ 1 ││ / \ ││ 5 3 5 > 2 ✗ (child smaller) ││ / \ / ││ 2 7 6 ││ │└─────────────────────────────────────────────────────┘The beauty of the heap is that these two simple properties are sufficient. Shape ensures O(log n) height. Ordering ensures the extremum is at the root. Together, they enable everything we need.
Why Completeness Matters:
A complete binary tree is filled level-by-level, left-to-right, with no gaps. This specific shape provides two crucial benefits:
Benefit 1: Guaranteed Height Bound
A complete binary tree with n nodes has height exactly ⌊log₂(n)⌋.
Proof intuition:
For n nodes, we need at least ⌊log₂(n)⌋ levels. Completeness ensures we don't need more.
Benefit 2: Array Representation
Because there are no gaps, we can store the tree in an array without wasted space. Element at index i has:
This eliminates pointer overhead entirely.
| Height | Max Nodes | Min Nodes | Height in O-notation |
|---|---|---|---|
| 0 | 1 | 1 | O(1) for n=1 |
| 1 | 3 | 2 | O(1) for n=2-3 |
| 2 | 7 | 4 | O(1) for n=4-7 |
| 3 | 15 | 8 | O(1) for n=8-15 |
| 10 | 2,047 | 1,024 | O(10) for n~1000-2000 |
| 20 | 2,097,151 | 1,048,576 | O(20) for n~1 million |
| 30 | ~1 billion | ~500 million | O(30) for n~1 billion |
Contrast with Unbalanced Trees:
Without the completeness constraint, a binary tree with n nodes could have height n-1 (a linked list in disguise). If heap operations took O(height) time, an unbalanced heap would have O(n) operations—no better than our naive approaches!
Unbalanced BST (worst case) vs Complete Binary Tree
1 1
\ /
2 2 3
\ / \ /
3 vs 4 5 6 7
4
5
Height: n-1 = 4 Height: log₂(7) ≈ 2.8
Completeness guarantees logarithmic height, which guarantees logarithmic operation time.
A complete binary tree is a specific type of balanced tree. All complete trees are balanced (height = O(log n)), but not all balanced trees are complete. AVL and Red-Black trees are balanced but allow gaps. Heaps specifically require completeness for the array representation.
The Heap Property Formally:
For a min-heap, for every node N (except the root):
N.value ≥ N.parent.value
Equivalently, for every parent P:
P.value ≤ P.leftChild.value (if left child exists)
P.value ≤ P.rightChild.value (if right child exists)
For a max-heap, reverse the inequalities.
What the Heap Property Guarantees:
What the Heap Property Does NOT Guarantee:
1234567891011121314151617181920212223242526272829303132333435363738394041
# Heap Property Examples (Min-Heap) ## Example 1: Valid Min-Heap 1 / \ 3 2 / \ / \ 7 6 4 5 Parent-child relationships all satisfy parent ≤ children:- 1 ≤ 3 ✓, 1 ≤ 2 ✓- 3 ≤ 7 ✓, 3 ≤ 6 ✓- 2 ≤ 4 ✓, 2 ≤ 5 ✓ Note: 3 > 2 (siblings unordered) — this is FINE!Note: 7 > 4 (cousins unordered) — this is FINE! ## Example 2: Invalid Min-Heap 1 / \ 4 2 / \ 3 6 Violation: 4 > 3 (parent > child) ✗The value 3 should "bubble up" past 4. ## Example 3: Deceptive Valid Heap 1 / \ 8 2 / \ / \ 9 10 3 4 This IS valid! Despite 8 appearing "too big":- 1 ≤ 8 ✓, 1 ≤ 2 ✓- 8 ≤ 9 ✓, 8 ≤ 10 ✓- 2 ≤ 3 ✓, 2 ≤ 4 ✓ The heap property is LOCAL (parent vs direct children).8 being much larger than its sibling 2 is irrelevant.A common misconception: heaps are 'almost sorted.' They're not! A heap only guarantees parent ≤ children—this is a very weak ordering. Two adjacent elements in the array may be in any order relative to each other. The heap property is just enough to efficiently find the minimum, nothing more.
Now for the key insight: how do the two heap properties combine to achieve O(log n) for insert and extract?
The Fundamental Operations:
Both insert and extract temporarily violate the heap property, then restore it:
Why O(log n)?
Bubbling up: Element moves from bottom toward root, at most log n levels. Bubbling down: Element moves from root toward bottom, at most log n levels.
Each level requires O(1) comparisons and swaps. The height is O(log n). Therefore, both operations are O(log n).
1234567891011121314151617181920212223242526272829303132333435363738
# Insert Operation Overview Initial Heap: Insert 0: After Bubble Up: Add at end 0 bubbles to root 1 1 0 / \ / \ / \ 3 2 3 2 1 2 / \ / / \ / \ / \ / 7 6 4 7 6 4 0 7 6 4 3 ↑ ↑ New position Bubble up Bubbling: Compare 0 with parent 2. 0 < 2, swap. Compare 0 with parent 1. 0 < 1, swap. 0 is now root. Done. Path length: 2 swaps = O(log n) # Extract Operation Overview Initial Heap: Remove root, After Bubble Down: Replace with last 1 4 2 / \ / \ / \ 3 2 3 2 3 4 / \ / / \ / \ 7 6 4 7 6 7 6 ↑ ↑ Last to root Bubble down Bubbling down: Compare 4 with children 3, 2. Swap with smaller (2). Compare 4 with children (none on left, none on right). 4 is a leaf. Done. Path length: 1 swap = O(log n)The Critical Invariant:
At every step of bubbling, we restore the heap property for an ever-larger subtree:
This local repair propagating upward/downward is what makes heaps efficient. We never examine more than O(log n) nodes.
| Operation | Complexity | Mechanism | Why It Works |
|---|---|---|---|
| insert | O(log n) | Add at end, bubble up | At most log n levels to traverse |
| extractMin | O(log n) | Move last to root, bubble down | At most log n levels to traverse |
| peekMin | O(1) | Return root | Heap property guarantees root is min |
| build heap | O(n) | Bottom-up heapify | Most nodes near bottom, short bubble distance |
| isEmpty | O(1) | Check array length | Trivial |
| size | O(1) | Return array length | Trivial |
The completeness property is crucial. It guarantees the tree has minimum height for n nodes. Without completeness (e.g., an arbitrary binary tree with heap ordering), the tree could be tall and skinny, and bubbling could take O(n) time.
The Brilliant Trick:
A complete binary tree can be stored in an array without wasting space and without needing pointers. This is called the implicit tree representation.
The Mapping:
For a 0-indexed array:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
/** * Tree: Array: * * 10 index: 0 1 2 3 4 5 6 * / \ value: 10 20 15 30 25 40 35 * 20 15 * / \ / \ * 30 25 40 35 * * Relationships (0-indexed): * - Node 10 at index 0: children at 1, 2 * - Node 20 at index 1: parent at 0, children at 3, 4 * - Node 15 at index 2: parent at 0, children at 5, 6 * - Node 30 at index 3: parent at 1, no children (3*2+1=7 > array.length) */ class BinaryHeap<T> { private data: T[] = []; private compare: (a: T, b: T) => number; constructor(compare: (a: T, b: T) => number) { this.compare = compare; } // Index calculations — the heart of array-based heaps private parent(i: number): number { return Math.floor((i - 1) / 2); } private leftChild(i: number): number { return 2 * i + 1; } private rightChild(i: number): number { return 2 * i + 2; } private hasLeftChild(i: number): boolean { return this.leftChild(i) < this.data.length; } private hasRightChild(i: number): boolean { return this.rightChild(i) < this.data.length; } private hasParent(i: number): boolean { return i > 0; } // Swapping elements — O(1) private swap(i: number, j: number): void { [this.data[i], this.data[j]] = [this.data[j], this.data[i]]; }}Why Arrays Work Here:
Normally, trees use explicit pointers (left, right, parent). Why can heaps use arrays?
Completeness: No gaps in the tree = no gaps in the array. Each index corresponds to exactly one node.
Predictable structure: The tree shape is completely determined by the number of elements. We don't need to store the structure—it's implicit in the indices.
Efficient navigation: Parent and child lookups are simple arithmetic—no pointer chasing, excellent cache performance.
Some textbooks use 1-indexed arrays (root at index 1) for prettier formulas: parent(i) = i/2, left(i) = 2i, right(i) = 2i+1. Both work; 0-indexed is more natural in most programming languages. Just be consistent.
Let's build a mental model that connects the tree visualization with the array storage. When you work with heaps, you'll think in both representations simultaneously.
Example: Building a Min-Heap
Starting with elements: [4, 10, 3, 5, 1]
Insert order and heap evolution:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
# Building a Min-Heap: Insert [4, 10, 3, 5, 1] ## Insert 4Tree: 4 Array: [4] No bubbling needed (single element). ## Insert 10 4 Array: [4, 10] / 10 10 ≥ 4 → heap property holds. ## Insert 3 4 / \ Array: [4, 10, 3] 10 3 3 < 4 → violation! Bubble up: 3 / \ Array: [3, 10, 4] 10 4 3 is now root. ## Insert 5 3 / \ Array: [3, 10, 4, 5] 10 4 / 5 5 < 10 → violation! Bubble up: 3 / \ Array: [3, 5, 4, 10] 5 4 / 10 ## Insert 1 3 / \ Array: [3, 5, 4, 10, 1] 5 4 / \ 10 1 1 < 5 → violation! Swap: 3 / \ Array: [3, 1, 4, 10, 5] 1 4 / \ 10 5 1 < 3 → still violation! Swap: 1 / \ Array: [1, 3, 4, 10, 5] 3 4 / \ 10 5 Final heap established!Reading the Array as a Tree:
The array [1, 3, 4, 10, 5] represents:
1 (index 0)
/
3 4 (indices 1, 2)
/
10 5 (indices 3, 4)
Verify the heap property:
All checks pass—it's a valid min-heap.
When debugging heaps, draw both the tree and array. Check the heap property in the tree view (visually inspect parent-child pairs). Verify index calculations in the array view. The two representations reinforce understanding.
A natural question arises: Binary Search Trees (BSTs) also provide O(log n) operations. Why use a heap instead of a BST for priority queues?
The Key Differences:
| Aspect | Binary Heap | Balanced BST |
|---|---|---|
| Structure | Complete binary tree | Balanced but can have gaps |
| Ordering | Parent ≤ children (partial) | Left < root < right (total) |
| Find min | O(1) — always at root | O(log n) — traverse to leftmost |
| Extract min | O(log n) | O(log n) |
| Insert | O(log n) | O(log n) |
| Find arbitrary | O(n) — must search entire heap | O(log n) — binary search |
| Storage | Array (cache-friendly) | Nodes with pointers |
| Memory overhead | Low (just the array) | Higher (pointers per node) |
| Implementation | Simple (~50 lines) | Complex (~200+ lines) |
When to Choose Each:
Use a Heap When:
Use a BST When:
The Priority Queue Sweet Spot:
For priority queues specifically, heaps win on almost every metric:
BSTs only win when you need operations heaps don't support (like searching for arbitrary elements). For pure priority queue use, BSTs are overkill.
This is a classic DSA pattern: stronger invariants enable more operations but cost more to maintain. Heaps maintain a weaker invariant (parent ≤ children) than BSTs (left < root < right), so heaps are cheaper to maintain but support fewer operations. Choose based on your requirements.
Standard libraries across languages provide heap implementations. Understanding their APIs helps you use them effectively.
Python: heapq Module
123456789101112131415161718192021222324252627282930
import heapq # heapq operates on regular lists, transforming them in-placeheap = [] # Insert: heappushheapq.heappush(heap, 5)heapq.heappush(heap, 3)heapq.heappush(heap, 7)heapq.heappush(heap, 1) print(heap) # [1, 3, 7, 5] — valid min-heap (not sorted!) # Peek: just access index 0print(heap[0]) # 1 # Extract: heappopprint(heapq.heappop(heap)) # 1print(heapq.heappop(heap)) # 3 # Build heap from list: heapifydata = [9, 5, 6, 2, 3]heapq.heapify(data) # O(n) in-place heapificationprint(data) # [2, 3, 6, 5, 9] # For max-heap: negate valuesmax_heap = []heapq.heappush(max_heap, -5) # Insert "5" as -5heapq.heappush(max_heap, -10)print(-heapq.heappop(max_heap)) # Extract, negate: 10Java: PriorityQueue
1234567891011121314151617181920212223242526272829303132
import java.util.PriorityQueue;import java.util.Comparator; public class HeapExample { public static void main(String[] args) { // Min-heap by default (natural ordering) PriorityQueue<Integer> minHeap = new PriorityQueue<>(); minHeap.offer(5); // insert minHeap.offer(3); minHeap.offer(7); System.out.println(minHeap.peek()); // 3 (minimum) System.out.println(minHeap.poll()); // 3 (extract) // Max-heap: provide reverse comparator PriorityQueue<Integer> maxHeap = new PriorityQueue<>( Comparator.reverseOrder() ); maxHeap.offer(5); maxHeap.offer(10); maxHeap.offer(3); System.out.println(maxHeap.poll()); // 10 (maximum) // Custom objects: use Comparator PriorityQueue<Task> taskQueue = new PriorityQueue<>( Comparator.comparingInt(task -> task.priority) ); }}JavaScript/TypeScript: No Built-in (Roll Your Own or Use Library)
12345678910111213141516171819
// JavaScript/TypeScript has no built-in heap!// Common approaches: // 1. Use a library like 'heap-js' or 'ts-priority-queue'import { MinHeap } from 'heap-js'; const heap = new MinHeap<number>();heap.push(5);heap.push(3);console.log(heap.pop()); // 3 // 2. Implement your own (coming in later modules!)class SimpleMinHeap { private data: number[] = []; push(val: number): void { /* implementation */ } pop(): number { /* implementation */ } peek(): number { return this.data[0]; }}Notice that Python's heapq is min-heap (and requires negation trick for max), while C++'s priority_queue is max-heap by default (and requires greater<T> for min). Always check the documentation for your language's default behavior.
Module Complete:
We've completed the journey from priority queue concept to heap solution:
What's Next:
The following modules in this chapter will dive into the implementation details:
You now understand the priority queue ADT, why naive implementations fail, and why heaps succeed. You can explain the two defining properties of a heap and why they enable O(log n) operations. This foundational understanding prepares you for the detailed implementation content ahead.