Implement Min Heap
Java source ↗ Edit this article ↗
Jump to code:
Problem
Implement a min heap of integers from scratch, without PriorityQueue. It must support:
initializeHeap(): start with an empty heap (in Java, the constructor).insert(key): addkey.getMin(): return the smallest element without removing it.extractMin(): remove and return the smallest element.changeKey(index, newVal): replace the element stored at positionindexof the heap’s array (0-based) withnewVal, then restore the heap.isEmpty()andheapSize().
Constraints: up to 10^5 operations, values fit in an int. getMin and extractMin are only called on a non-empty heap in the judge’s version; the code here throws NoSuchElementException on an empty heap so misuse is loud. changeKey is defined in terms of the array layout, so its effect depends on where the implementation keeps each element.
Example 1
insert(5), insert(3), insert(8), getMin(), insert(1), heapSize(), extractMin(), extractMin(), getMin()
output: 3, 4, 1, 3, 5 // the minimum always comes out first, whatever the insertion order
Example 2
insert(4), insert(7), insert(9), insert(10) // heap array is [4, 7, 9, 10]
changeKey(3, 1) // 10 becomes 1 and climbs: [1, 4, 9, 7]
changeKey(0, 20) // the root becomes 20 and sinks: [4, 7, 9, 20]
getMin() -> 4
Both implementations below satisfy one small interface, so a single test script drives them:
interface MinHeapOps {
void insert(int key);
int getMin();
int extractMin();
void changeKey(int index, int newVal);
boolean isEmpty();
int heapSize();
}
Approach 1: Unsorted array
Intuition. The simplest container that can hold a multiset is an array with a size counter. Insertion just appends. The price is paid when someone asks for the minimum: nothing is ordered, so we must scan everything. Removal is cheap once the minimum is found, because order does not matter: move the last element into the hole and shrink the size.
Algorithm.
- Keep
data(doubled when full) andsize. insert(key): writedata[size++] = key.getMin(): scandata[0..size-1]for the index of the smallest value and return it.extractMin(): find that indexi, rememberdata[i], then copydata[size - 1]into slotiand decrementsize.changeKey(i, v): just overwritedata[i], since there is no invariant to repair.
static class UnsortedArrayHeap implements MinHeapOps {
private int[] data = new int[4];
private int size = 0;
public void insert(int key) {
if (size == data.length) data = Arrays.copyOf(data, size * 2);
data[size++] = key;
}
private int minIndex() {
if (size == 0) throw new NoSuchElementException("heap is empty");
int best = 0;
for (int i = 1; i < size; i++) {
if (data[i] < data[best]) best = i;
}
return best;
}
public int getMin() {
return data[minIndex()];
}
public int extractMin() {
int i = minIndex();
int min = data[i];
data[i] = data[--size]; // fill the hole with the last element
return min;
}
public void changeKey(int index, int newVal) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException("index " + index);
data[index] = newVal; // there is no order to restore
}
public boolean isEmpty() {
return size == 0;
}
public int heapSize() {
return size;
}
}
Complexity. insert and changeKey are O(1) (amortised for the occasional doubling). getMin and extractMin are O(n) because each scans the whole array. Space O(n).
A workload that alternates inserts and extract-mins, which is exactly how heaps are used in Dijkstra’s algorithm or k-way merging, costs O(n^2) here. Keeping the array fully sorted would just move the cost: getMin becomes O(1) but insert must shift elements, O(n) again. We need a structure that is “sorted enough” to find the minimum instantly, yet cheap to repair.
Approach 2: Binary min-heap
Intuition. A binary heap is a complete binary tree stored level by level in an array, where every node is at most its children. Node i has children 2i + 1 and 2i + 2 and parent (i - 1) / 2. The order rule chains along every root-to-leaf path, so the root heap[0] is the minimum. The tree has height floor(log2 n), and every modification can be repaired by walking a single path up or down, never touching the rest of the tree.
- Insert. Put the key in slot
size, the only slot that keeps the tree complete. Only the edge to its parent can be wrong, so swap it upward while it is smaller than its parent (sift up). - Extract. Save the root, move the last element into the root, shrink. Now only the root may be too large, so swap it with its smaller child while some child is smaller (sift down). The smaller child is the only one that can sit above the other.
- Change key. If the new value is smaller than the old one, the subtree below is still fine (its parent got smaller), but the node may now be smaller than its own parent: sift up. If the value grew, the path above is still fine, but the node may exceed a child: sift down. Exactly one direction can be broken, so one walk repairs it.
Algorithm.
insert(key): grow the array if full, store atheap[size], incrementsize, sift up fromsize - 1.getMin(): returnheap[0].extractMin(): saveheap[0], decrementsize, copyheap[size]toheap[0], sift down from 0, return the saved value.changeKey(i, v): storev; sift up if it decreased, otherwise sift down.- Sift up stops when the parent is
<=the node; sift down stops when neither child is smaller.
static class BinaryMinHeap implements MinHeapOps {
private int[] heap = new int[4];
private int size = 0;
public void insert(int key) {
if (size == heap.length) heap = Arrays.copyOf(heap, size * 2);
heap[size] = key;
size++;
siftUp(size - 1);
}
public int getMin() {
if (size == 0) throw new NoSuchElementException("heap is empty");
return heap[0];
}
public int extractMin() {
if (size == 0) throw new NoSuchElementException("heap is empty");
int min = heap[0];
size--;
heap[0] = heap[size]; // last leaf becomes the root
siftDown(0);
return min;
}
public void changeKey(int index, int newVal) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException("index " + index);
int old = heap[index];
heap[index] = newVal;
if (newVal < old) siftUp(index);
else siftDown(index);
}
public boolean isEmpty() {
return size == 0;
}
public int heapSize() {
return size;
}
private void siftUp(int i) {
while (i > 0) {
int p = (i - 1) / 2;
if (heap[p] <= heap[i]) break;
swap(i, p);
i = p;
}
}
private void siftDown(int i) {
while (true) {
int smallest = i, l = 2 * i + 1, r = 2 * i + 2;
if (l < size && heap[l] < heap[smallest]) smallest = l;
if (r < size && heap[r] < heap[smallest]) smallest = r;
if (smallest == i) return;
swap(i, smallest);
i = smallest;
}
}
private void swap(int i, int j) {
int t = heap[i];
heap[i] = heap[j];
heap[j] = t;
}
int[] snapshot() {
return Arrays.copyOf(heap, size);
}
}
Dry run of Example 2 on the binary heap:
| operation | array before repair | repair | array after |
|---|---|---|---|
| insert 4, 7, 9, 10 | [4, 7, 9, 10] | each new key is at least its parent | [4, 7, 9, 10] |
| changeKey(3, 1) | [4, 7, 9, 1] | 1 < parent 7: swap; 1 < parent 4: swap | [1, 4, 9, 7] |
| changeKey(0, 20) | [20, 4, 9, 7] | smaller child 4: swap; child 7 < 20: swap | [4, 7, 9, 20] |
| extractMin() → 4 | [20, 7, 9] | smaller child 7: swap | [7, 20, 9] |
Complexity. insert, extractMin and changeKey are O(log n): each walks one path of a tree of height floor(log2 n). getMin, isEmpty and heapSize are O(1). Space O(n) for the array, which doubles when full so insertion stays amortised O(log n).
| operation | unsorted array | binary heap |
|---|---|---|
| insert | O(1) | O(log n) |
| getMin | O(n) | O(1) |
| extractMin | O(n) | O(log n) |
| changeKey | O(1) | O(log n) |
The tests run a seeded random mix of 4000 inserts and extracts against java.util.PriorityQueue for both implementations, and 300 random changeKey calls on the binary heap, checking the heap property after each one.
Edge cases and pitfalls
- Empty heap.
getMinandextractMinthrowNoSuchElementException; the heap remains usable afterwards. If your judge expects-1orInteger.MAX_VALUEinstead, change only those two guards. - Extracting the last element. With one element,
sizedrops to 0,heap[0] = heap[0]is harmless, and sift down returns immediately because no child index is belowsize. - Duplicates. Sift up stops at
heap[p] <= heap[i], so equal keys never swap. Using<there would still be correct but would do pointless swaps. - Picking the wrong child. Sift down must compare with the smaller child. Swapping with the left child by default can leave the right child smaller than its new parent.
changeKeysemantics. The index refers to the internal array, which the heap rearranges after every operation. Callers that need to change “the key of item X” keep a position map (item to index) updated insideswap, as indexed priority queues in Dijkstra implementations do.- Bounds.
changeKeywith an index outside[0, size)throwsIndexOutOfBoundsExceptioninstead of corrupting the array.
Related problems
- Heaps (Theory Video) explains the shape and order rules, heapify and
PriorityQueue. - Implement Max Heap is the same class with every comparison flipped.
- Check if an array represents a min heap verifies the invariant this class maintains.
- Convert Min Heap to Max Heap rebuilds a heap in
O(n)with repeated sift down. - Implement Stack using Arrays uses the same growable-array storage for a simpler container.
- Implement Min Stack also answers
getMininO(1), but only for last-in, first-out removal.