Trees

Heap Fundamentals

A complete binary tree with one structural guarantee β€” the heap property: in a min-heap every parent is less than or equal to its children, in a max-heap every parent is greater than or equal. This invariant keeps the extreme value at the root for O(1) access, while the complete-tree shape enables O(1) array storage with no pointers β€” making heaps the engine behind priority queues, Dijkstra's algorithm, and heap sort.

Foundations Β· Array Β· Linked List Β· Stack Β· Queue Β· HashMap Β· Binary Tree Β· BST Β· Heap Β· Trie Β· Graph Β· Sorting Β· Binary Search Β· Two Pointers Β· Sliding Window Β· Dynamic Prog.
01
Section One Β· Foundation

What is a Heap?

1 min 3 5 7 9 8 12 complete tree 3 < 7 3 < 9 heap property min-heap: parent ≀ children at every node

A heap is a complete binary tree satisfying the heap property β€” in a min-heap, every parent node's value is less than or equal to its children's values; in a max-heap, every parent is greater than or equal to its children. The complete-tree property means all levels are fully filled except possibly the last, which fills left-to-right β€” and this structural guarantee enables a remarkably efficient trick: the entire tree can be stored in a flat array with no pointers. The root sits at index 1, the left child of node i at 2i, the right child at 2i+1, and the parent at ⌊i/2βŒ‹ β€” pure index arithmetic replaces every pointer dereference. The heap property guarantees that the minimum (or maximum) is always at the root, giving O(1) access to the extreme value. Insert and extract operations maintain both invariants in O(log n) time by bubbling up (swap with parent until the heap property holds) or bubbling down (swap with the smaller child until the heap property holds). Java's PriorityQueue is a binary min-heap under the hood. Heaps power Dijkstra's shortest-path algorithm, heap sort, and any problem requiring repeated extraction of the minimum or maximum from a dynamic collection. Understanding heaps means understanding how array index arithmetic replaces pointer navigation entirely.

Analogy: A hospital emergency triage queue β€” patients are always treated in order of severity (priority), not arrival time. Adding a new patient inserts them at the right severity level (bubble up), and treating a patient removes the most critical one and reorganises the queue (bubble down). The head of the queue is always the most urgent case β€” that is your heap root. You never scan every patient to find the most critical; the triage system guarantees the most urgent is always at the front. A heap provides the exact same guarantee for data.
02
Section Two Β· Mental Model

How It Thinks

Complete binary tree property
β–Ά
Enables array storage with O(1) index arithmetic β€” no pointers, no wasted space, cache-friendly sequential memory layout
Heap property: parent ≀ children (min-heap)
β–Ά
Minimum is always at the root β€” O(1) peek at the global minimum without any search or traversal
Insert: add at end, bubble up
β–Ά
Maintains complete tree shape first, then restores heap property by swapping upward β€” O(log h) = O(log n)
Extract-min: remove root, move last to root, bubble down
β–Ά
Minimum removed, complete-tree shape maintained, heap property restored by swapping downward β€” O(log n)
Height is always O(log n)
β–Ά
Because the tree is always complete β€” no degenerate case exists, performance is guaranteed unlike an unbalanced BST
Build heap from array in O(n)
β–Ά
Heapify from bottom-up is more efficient than n individual inserts β€” the Floyd algorithm insight that most nodes are near the leaves
03
Section Three Β· Heap Properties

The Two Invariants β€” Shape and Order

Heaps have two invariants that must hold simultaneously β€” the shape invariant (complete binary tree) and the order invariant (heap property). Understanding these separately is the key to understanding every heap operation: insert restores shape first then order, extract-min restores order after replacing the root with the last element. Every bug in a heap implementation traces back to violating one of these two invariants.

Heap anatomy β€” tree, array mapping, and index arithmetic
1 [1] 3 [2] 5 [3] 7 [4] 9 [5] 8 [6] 12 [7] 15 [8] 10 [9] left-to-right fill node [3] (val=5) left = 2Γ—3 = [6] right = 2Γ—3+1 = [7] parent = 3/2 = [1] Array: _ 0 1 1 3 2 5 3 7 4 9 5 8 6 12 7 15 8 10 9 [_, 1, 3, 5, 7, 9, 8, 12, 15, 10] β€” index 0 unused (1-based indexing)
Heap Properties Reference
Property Definition Example
Heap property (min) parent.val ≀ children.val for all nodes 3 ≀ 7 and 3 ≀ 9
Heap property (max) parent.val β‰₯ children.val for all nodes 9 β‰₯ 7 and 9 β‰₯ 3
Complete tree All levels full except last; last fills left-to-right Nodes at indices 1..n
Array mapping Node i: left=2i, right=2i+1, parent=⌊i/2βŒ‹ Node 3: left=6, right=7
Root is extremum Min-heap root = global minimum; max-heap root = global maximum heap[1] is always min
Height Always ⌊logβ‚‚ nβŒ‹ β€” guaranteed O(log n) 7 nodes β†’ height=2
Size n nodes occupy indices 1..n; index 0 unused Array size = n+1
Why index 0 is unused:
  • Using 1-based indexing makes the parent/child arithmetic clean β€” parent = i/2 with integer division, left = 2i, right = 2i+1.
  • If 0-based indexing is used instead, the formulas become left = 2i+1, right = 2i+2, parent = (iβˆ’1)/2 β€” correct but slightly messier.
  • Java's PriorityQueue uses 0-based indexing internally; most textbook heaps use 1-based for clarity.
04
Section Four Β· Heap Types

Min-Heap, Max-Heap, and the Two-Heap Pattern

Three heap variants appear in interviews and systems β€” min-heap, max-heap, and the two-heap pattern. Java's PriorityQueue is a min-heap by default; a max-heap requires a reversed comparator. The two-heap pattern (one min-heap + one max-heap) is the algorithmic key to median-finding problems and appears in several hard interview questions.

Min-Heap vs Max-Heap vs Two-Heap Pattern
MIN-HEAP 1 3 5 7 9 root = minimum parent ≀ children new PriorityQueue<>() MAX-HEAP 9 7 5 3 1 root = maximum parent β‰₯ children new PriorityQueue<>(reverseOrder()) TWO-HEAP (Median) max-heap (lower) 3 1 2 median = 3 or 4 min-heap (upper) 4 5 7 median = max of lower half pattern: find running median
Heap Type Comparison
Type Root holds Java creation Use case
Min-Heap Global minimum new PriorityQueue<>() Dijkstra, K smallest, scheduling
Max-Heap Global maximum new PriorityQueue<>(Comparator.reverseOrder()) K largest, sliding window max
Two-Heap β€” One of each Running median, data stream
D-ary Heap Min or Max Custom Decrease-key heavy workloads
K largest β†’ use a min-heap of size K:
  • Counterintuitive but correct β€” maintaining a min-heap of the K largest elements seen so far lets you evict the smallest of the K whenever a new element arrives.
  • The heap top is always the Kth largest element.
  • For K smallest, the mirror applies: use a max-heap of size K and evict the largest when a new smaller element arrives.
05
Section Five Β· Core Operations

Insert, Extract, Peek, and Decrease-Key

Insert (Bubble Up)
Add element at the next available position (end of array), then swap with parent while the heap property is violated β€” O(log n).
Pseudocode
function insert(heap, val): append val to end of heap // maintain complete tree shape i = last index while i > 1 and heap[i] < heap[parent(i)]: swap(heap[i], heap[parent(i)]) i = parent(i) // bubble up // O(log n) β€” at most h swaps
Insert β€” adding value 2 into min-heap [1, 3, 5, 7, 9]
STEP 1: before 1 3 5 7 9 [_,1,3,5,7,9] STEP 2: append 2 1 3 5 7 9 2 2 < 5 βœ— [_,1,3,5,7,9,2] STEP 3: swap 2↔5 1 3 2 7 9 5 2 < 3? no swap with 1 [_,1,3,2,7,9,5] STEP 4: done βœ“ 1 3 2 7 9 5 [_,1,3,2,7,9,5] β†’ β†’ β†’
Bubble-up only travels one root-to-leaf path:
  • β€” at most logβ‚‚n swaps. The complete tree property guarantees this path is short.
  • No matter how large the heap, insert never visits more nodes than the height of the tree.
Extract-Min (Bubble Down)
Remove and return the root (minimum), replace it with the last element, then swap with the smaller child while the heap property is violated β€” O(log n).
Pseudocode
function extractMin(heap): min = heap[1] // save root heap[1] = heap[last] // move last to root remove last element // shrink heap i = 1 while true: smallest = i if left(i) exists and heap[left(i)] < heap[smallest]: smallest = left(i) if right(i) exists and heap[right(i)] < heap[smallest]: smallest = right(i) if smallest == i: break // heap property restored swap(heap[i], heap[smallest]) i = smallest // bubble down // O(log n)
Extract-min from heap [1, 3, 5, 7, 9, 8]
STEP 1: remove root 1 remove 3 5 7 9 8 [_,1,3,5,7,9,8] STEP 2: lastβ†’root 8 violation 3 5 7 9 [_,8,3,5,7,9] 8 > 3 β€” swap with smaller child STEP 3: done βœ“ 3 7 5 8 9 [_,3,7,5,8,9] β†’ β†’
Always swap with the SMALLER child in a min-heap:
  • β€” otherwise you promote a value larger than the other child, creating a new violation.
  • In a max-heap, swap with the LARGER child. This is the most common implementation bug in heap operations.
Peek
Return the root without removing it β€” O(1).
Pseudocode
function peek(heap): if heap is empty: throw exception return heap[1] // root is always the minimum // O(1)
Peek is O(1) because the heap property guarantees the minimum is always at the root.:
  • No traversal needed.
  • This is the entire reason to use a heap over a sorted array when you only need repeated min/max access β€” the sorted array gives O(1) min but O(n) insert, while the heap gives O(1) peek and O(log n) insert.
Decrease-Key / Update Priority
Update a node's value to a smaller value (min-heap), then bubble up to restore heap property β€” O(log n). Used in Dijkstra's algorithm.
Pseudocode
function decreaseKey(heap, i, newVal): if newVal > heap[i]: throw error // can only decrease heap[i] = newVal while i > 1 and heap[i] < heap[parent(i)]: swap(heap[i], heap[parent(i)]) i = parent(i) // same as bubble-up // O(log n)
Java's PriorityQueue does NOT support decrease-key efficiently:
  • β€” there's no O(log n) update.
  • The workaround is lazy deletion: add the updated node as a new entry and mark the old entry as stale, ignoring stale entries on extraction.
  • This is the standard approach in Dijkstra implementations with Java's PriorityQueue.
Operations Summary
Operation Time Space Notes
peek (min/max) O(1) O(1) Root is always the extremum
insert O(log n) O(1) Bubble up at most h levels
extract-min/max O(log n) O(1) Bubble down at most h levels
build heap O(n) O(1) Floyd's heapify β€” not n inserts
decrease-key O(log n) O(1) Not supported directly in Java
search O(n) O(1) No ordering beyond parent>children
Common Mistake: Calling PriorityQueue.remove(element) to update priorities β€” this is O(n) because it searches linearly. For Dijkstra with frequent updates, use the lazy deletion pattern (re-add with new priority, skip stale extractions) to stay O((V+E) log V).
06
Section Six Β· Heapify

Heapify β€” Building a Heap in O(n)

The naΓ―ve approach to building a heap is n individual inserts β€” O(n log n). Floyd's heapify algorithm builds a valid heap from an unsorted array in O(n) by starting from the last internal node (index n/2) and calling sift-down toward index 1. Leaves are already valid 1-node heaps; sift-down on higher nodes is cheap because subtrees below are already heapified. The O(n) proof is non-obvious but important: most nodes are near the leaves where sift-down costs O(1).

Sift-Down β€” O(log n)
function siftDown(heap, i, size): while true: smallest = i l = 2*i; r = 2*i+1 if l <= size and heap[l] < heap[smallest]: smallest = l if r <= size and heap[r] < heap[smallest]: smallest = r if smallest == i: break swap(heap[i], heap[smallest]) i = smallest
Build Heap β€” O(n)
function buildHeap(array): n = array.length // start from last internal node, go to root for i from n/2 down to 1: siftDown(array, i, n) // array is now a valid min-heap
Floyd's heapify on array [9, 4, 7, 2, 8, 1, 5] (7 nodes)
STAGE 1: unsorted 9 4 7 2 8 1 5 [_,9,4,7,2,8,1,5] STAGE 2: sift i=3,2 9 2 1 4 8 7 5 [_,9,2,1,4,8,7,5] 4↔2 7↔1 STAGE 3: sift i=1 βœ“ 1 2 5 4 8 7 9 [_,1,2,5,4,8,7,9] β†’ β†’
O(n log n) β€” Naive Build

n individual inserts, each O(log n). Simple but suboptimal. Acceptable when building from a stream of incoming elements where you must maintain heap order after every arrival.

O(n) β€” Floyd's Heapify

Start from last internal node, sift-down to root. Each sift-down costs O(height of subtree). The sum of subtree heights = O(n) by a geometric series argument. Use when building from a known array.

The O(n) complexity proof:
  • Sum the heights of all nodes β€” there are n/2 leaves (height 0), n/4 nodes at height 1, n/8 at height 2, and so on.
  • The total work is n/2Β·0 + n/4Β·1 + n/8Β·2 + ... which converges to O(n) by a geometric series.
  • This is why heapify beats n individual inserts β€” most of the work happens at the leaves where the cost is zero.
07
Section Seven Β· Heap Sort

Heap Sort β€” O(n log n) In-Place

Heap sort combines build-heap (O(n)) with n extract-max operations (O(log n) each) to produce a sorted array in O(n log n) time with O(1) extra space. Unlike merge sort, it requires no extra array. Unlike quicksort, it has guaranteed O(n log n) worst-case. The key insight: after extract-max, place the maximum at the array's end β€” the sorted portion grows from right to left while the heap shrinks from right to left.

Pseudocode
function heapSort(array): // Phase 1: Build max-heap in-place β€” O(n) buildMaxHeap(array) // Phase 2: Extract max n-1 times β€” O(n log n) for i from n down to 2: swap(array[1], array[i]) // move max to sorted position siftDown(array, 1, i-1) // restore heap on shrunk array // array is now sorted ascending
Heap sort on [3, 1, 4, 1, 5]
STEP 1: build max-heap 5 3 4 1 1 [5, 3, 4, 1, 1] STEP 2: swap root, sift 4 3 1 1 5 sorted [4, 3, 1, 1 | 5] STEP 3: continue 3 1 1 4 5 sorted [3, 1, 1 | 4, 5] Final: [1, 1, 3, 4, 5] βœ“ β†’ β†’
Sorting Algorithm Comparison
Algorithm Time (best) Time (avg) Time (worst) Space Stable
Heap Sort O(n log n) O(n log n) O(n log n) O(1) No
Merge Sort O(n log n) O(n log n) O(n log n) O(n) Yes
Quick Sort O(n log n) O(n log n) O(nΒ²) O(log n) No
Why heap sort loses in practice:
  • Heap sort is rarely used despite its theoretical advantages.
  • Its non-sequential memory access pattern (jumping between parent and child indices in the array) causes poor cache performance.
  • QuickSort, with its sequential access patterns, typically outperforms heap sort by 2–5Γ— on real hardware despite the worst-case risk.
  • Java's Arrays.sort() uses a dual-pivot quicksort for primitives and TimSort (merge sort variant) for objects.
08
Section Eight Β· Implementation

Build It Once

Build this from scratch once β€” the array index arithmetic becomes mechanical after one implementation. In interviews, Java's PriorityQueue is the tool; understanding the internals makes you dangerous with it.

Java β€” MinHeap core mechanics
// Min-heap backed by an int array β€” 1-based indexing // Root at index 1; left child of i = 2i; right = 2i+1; parent = i/2 public class MinHeap { private int[] heap; private int size; public MinHeap(int capacity) { heap = new int[capacity + 1]; // index 0 unused size = 0; } // INSERT β€” O(log n) public void insert(int val) { heap[++size] = val; bubbleUp(size); } // EXTRACT-MIN β€” O(log n) public int extractMin() { if (size == 0) throw new NoSuchElementException(); int min = heap[1]; heap[1] = heap[size--]; bubbleDown(1); return min; } // PEEK β€” O(1) public int peek() { return heap[1]; } }
Common Mistake: Never call new PriorityQueue<>(array) and expect it to work β€” PriorityQueue has a constructor that takes a Collection, so new PriorityQueue<>(Arrays.asList(array)) builds a heap from the list in O(n). But if you pass an int[] directly, Java sees it as a capacity hint, not initial elements.
09
Section Nine Β· Java Reference

Use It In Java

IN JAVA β€” PriorityQueue<E>
PriorityQueue<E> β€” binary min-heap by default

PriorityQueue<E> in java.util β€” binary min-heap by default. For max-heap, pass Comparator.reverseOrder(). Implements the Queue interface. Unbounded (grows automatically).

// MIN-HEAP (default) PriorityQueue<Integer> minHeap = new PriorityQueue<>(); minHeap.offer(5); // insert β€” O(log n) minHeap.offer(3); minHeap.offer(7); minHeap.peek(); // view min β€” O(1), returns null if empty minHeap.poll(); // remove min β€” O(log n), returns null if empty minHeap.size(); // O(1) minHeap.isEmpty(); // O(1) // MAX-HEAP PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder()); // CUSTOM OBJECTS β€” sort by field PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); // by first element // BUILD FROM COLLECTION β€” O(n) PriorityQueue<Integer> fromList = new PriorityQueue<>(Arrays.asList(3, 1, 4, 1, 5));
PriorityQueue Methods
Method Time Notes
offer(e) / add(e) O(log n) add throws on capacity limit (unbounded: same)
peek() O(1) Returns null if empty; use for min-heap top
poll() O(log n) Returns null if empty
remove(Object o) O(n) Linear scan β€” avoid for priority updates
contains(Object o) O(n) Linear scan β€” heap has no search structure
size() O(1)
toArray() O(n) Array is NOT sorted β€” heap order only
⚠ Gotcha: PriorityQueue.toArray() and iterating a PriorityQueue with a for-each loop do NOT return elements in sorted order β€” they return elements in internal heap order. To get sorted output, repeatedly poll() until empty. This is a common interview mistake.
When to use PriorityQueue

Use PriorityQueue for any problem requiring repeated access to the minimum or maximum element: K largest/smallest, merge K sorted lists, Dijkstra's shortest path, task scheduling by priority, sliding window maximum. For static sorted data, use Arrays.sort() or Collections.sort() instead β€” a heap is only advantageous when elements arrive or are removed dynamically.

⚠ Gotcha: The comparator subtraction pattern (a, b) -> a - b overflows when a and b have opposite signs and large magnitudes. Use Integer.compare(a, b) or Comparator.comparingInt(x -> x[0]) for safety in production code and interviews.
10
Section Ten Β· Practice

Problems To Solve

Heap problems in interviews break into three patterns: K-element problems (use a heap of size K to find K largest/smallest in a stream), merge problems (use a min-heap to merge K sorted arrays or lists by always extracting the global minimum), and graph problems (Dijkstra's algorithm uses a min-heap to always process the nearest unvisited node). Recognise which pattern applies from the problem statement β€” "K largest", "K smallest", "K closest", "merge K sorted" β€” and the heap structure follows immediately.

Difficulty Pattern Problem Key Insight
Easy heap LC 703 Β· Kth Largest Element in a Stream Maintain a min-heap of size K; the top is always the Kth largest seen so far. On each new element, offer it, then poll if size exceeds K.
Easy heap LC 1046 Β· Last Stone Weight Simulate by repeatedly extracting the two largest stones from a max-heap and inserting the remainder if nonzero. Terminate when one or zero stones remain.
Medium heap LC 973 Β· K Closest Points to Origin Min-heap by distance, extract K times. Or max-heap of size K: evict the farthest point when size exceeds K β€” the heap top is the Kth closest.
Medium heap LC 295 Β· Find Median from Data Stream Two-heap pattern: max-heap for lower half, min-heap for upper half; rebalance after each insert so sizes differ by at most 1. Median is the top of the larger heap (or average of both tops).
Hard heap LC 23 Β· Merge K Sorted Lists Min-heap storing (value, list-index, node-index); always extract the global minimum across all lists and advance that list's pointer. O(N log K) where N is total elements.
Medium heap+graph LC 743 Β· Network Delay Time Min-heap on (distance, node); extract nearest, relax neighbours, skip already-visited nodes. Classic single-source shortest path β€” O((V+E) log V) with a binary heap.
The Two-Heap pattern:
  • (max-heap for lower half + min-heap for upper half) appears in several hard problems.
  • After each insert, enforce the invariant: sizes differ by at most 1 and max-heap.top ≀ min-heap.top.
  • If either is violated, move the top of the larger heap to the smaller one.
  • This gives O(log n) insert and O(1) median access.

β†’ See all Heap problems with full solutions