Implement Max Heap
Java source ↗ Edit this article ↗
Jump to code:
Problem
Implement a max heap of integers from scratch, without java.util.PriorityQueue. It must support:
insert(key): addkey.getMax(): return the largest element without removing it.extractMax(): remove and return the largest element.changeKey(index, newVal): replace the element stored at positionindex(0-based) of the heap’s internal array withnewVal, then restore the heap.isEmpty()andheapSize().
A useful extra, covered below, is building a heap from an existing array in linear time.
Constraints: up to 10^5 operations, values fit in an int, duplicates allowed. The judge only calls getMax and extractMax on a non-empty heap; the code here throws NoSuchElementException on an empty heap so misuse is loud rather than silent.
Example 1
insert(5), insert(3), insert(8), getMax(), insert(10), heapSize(), extractMax(), extractMax(), getMax()
output: 8, 4, 10, 8, 5 // the largest element always comes out first
Example 2
insert(10), insert(9), insert(7), insert(4) // heap array is [10, 9, 7, 4]
changeKey(3, 15) // 4 becomes 15 and climbs: [15, 10, 7, 9]
changeKey(0, 1) // the root becomes 1 and sinks: [10, 9, 7, 1]
getMax() -> 10
Both implementations satisfy one interface, so a single test script drives them and compares them with PriorityQueue as an oracle:
interface MaxHeapOps {
void insert(int key);
int getMax();
int extractMax();
void changeKey(int index, int newVal);
boolean isEmpty();
int heapSize();
}
Approach 1: Brute force, unsorted array
Intuition. The simplest container for a multiset is a growable array with a size counter. Insertion appends in O(1). All the cost moves to the queries: the elements are in no particular order, so finding the maximum means scanning everything. Removal is cheap once the maximum is found, because order does not matter: copy the last element into the hole and shrink the size.
Algorithm.
insert: double the array if full, then write the key atsizeand incrementsize.getMax: scan indices0..size-1for the largest value.extractMax: find the index of the maximum, remember its value, overwrite it with the last element, decrementsize.changeKey: overwrite the slot; no ordering exists to repair.
static class UnsortedArrayHeap implements MaxHeapOps {
private int[] data = new int[8];
private int size = 0;
public void insert(int key) {
if (size == data.length) data = Arrays.copyOf(data, size * 2);
data[size++] = key;
}
private int maxIndex() {
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 getMax() {
return data[maxIndex()];
}
public int extractMax() {
int i = maxIndex();
int max = data[i];
data[i] = data[--size]; // order is irrelevant: fill the hole with the last element
return max;
}
public void changeKey(int index, int newVal) {
if (index < 0 || index >= size) throw new IndexOutOfBoundsException("index " + index);
data[index] = newVal;
}
public boolean isEmpty() {
return size == 0;
}
public int heapSize() {
return size;
}
}
Complexity. insert and changeKey are O(1) (amortised for the doubling). getMax and extractMax are O(n). Space O(n). A workload that alternates inserts and extracts costs O(n^2) overall, which is what a heap is designed to avoid.
Approach 2: Optimal, binary max-heap in an array
Intuition. A binary max-heap is a complete binary tree in which every node is at least as large as its children. “Complete” means every level is full except possibly the last, which fills from the left. That shape lets us store the tree level by level in an array with no pointers:
- the children of index
iare2i + 1and2i + 2, - the parent of index
iis(i - 1) / 2.
The order rule chains along every root-to-leaf path, so the root heap[0] is the maximum. A complete tree with n nodes has height floor(log2 n), and every update below repairs the order along a single path, so each costs O(log n).
Two repair moves do all the work:
- Sift up: a node that may be bigger than its parent swaps with the parent until it is not, or it reaches the root.
- Sift down: a node that may be smaller than a child swaps with its larger child until both children are no bigger, or it becomes a leaf. Swapping with the larger child matters: that child becomes the parent of the other, so it must be the bigger of the two.
Algorithm.
insert: place the key at the first free slot (the next leaf), then sift it up.getMax: returnheap[0].extractMax: saveheap[0], move the last leaf into the root, shrink the size, sift the root down.changeKey: overwrite the slot. If the value grew, it can only violate the rule with its parent, so sift up; if it shrank, it can only violate it with its children, so sift down.- Build from an array: copy the values, then sift down every internal node from the last one (
size / 2 - 1) back to the root.
static class BinaryMaxHeap implements MaxHeapOps {
private int[] heap;
private int size;
BinaryMaxHeap() {
heap = new int[8];
}
/** Builds a heap from arbitrary values in O(n) by sifting down every internal node, last one first. */
BinaryMaxHeap(int[] values) {
heap = Arrays.copyOf(values, Math.max(8, values.length));
size = values.length;
for (int i = size / 2 - 1; i >= 0; i--) siftDown(i);
}
public void insert(int key) {
if (size == heap.length) heap = Arrays.copyOf(heap, size * 2);
heap[size] = key;
siftUp(size++);
}
public int getMax() {
if (size == 0) throw new NoSuchElementException("heap is empty");
return heap[0];
}
public int extractMax() {
if (size == 0) throw new NoSuchElementException("heap is empty");
int max = heap[0];
heap[0] = heap[--size]; // move the last leaf to the root
siftDown(0); // and let it sink to its place
return max;
}
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); // bigger: may beat its parent
else siftDown(index); // smaller: may lose to a child
}
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(p, i);
i = p;
}
}
private void siftDown(int i) {
while (true) {
int largest = i, l = 2 * i + 1, r = 2 * i + 2;
if (l < size && heap[l] > heap[largest]) largest = l;
if (r < size && heap[r] > heap[largest]) largest = r;
if (largest == i) return;
swap(i, largest);
i = largest;
}
}
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);
}
}
snapshot() exposes the array for the tests, which check the exact layouts in Example 2 and the heap property after random changeKey calls.
Dry run: insert 5, 3, 8, 10, then extractMax().
| step | array after the step | what happened |
|---|---|---|
| insert 5 | [5] | root |
| insert 3 | [5, 3] | 3 ≤ parent 5, stays |
| insert 8 | [8, 3, 5] | 8 > parent 5 at index 0, swap |
| insert 10 | [10, 8, 5, 3] | 10 swaps with 3 (index 1), then with 8 (index 0) |
| extractMax → 10 | [8, 3, 5] | last leaf 3 moves to the root, swaps with larger child 8 |
Why building is O(n) and not O(n log n). Inserting n values one by one costs O(n log n). Bottom-up building is cheaper because most nodes are near the bottom: about n/2 nodes are leaves and need no work, about n/4 sit one level up and sift down at most one level, about n/8 at most two levels, and so on. The total is n · (1/4 + 2/8 + 3/16 + ...), which is at most n. Leaves are skipped entirely by starting at index size / 2 - 1, the last node that has a child.
Complexity. insert, extractMax and changeKey are O(log n); getMax, isEmpty and heapSize are O(1); building from an array is O(n). Space O(n) for the array, which doubles when full so insertion stays amortised O(log n).
Edge cases and pitfalls
- Empty heap.
getMaxandextractMaxthrowNoSuchElementException; the tests confirm the heap is still usable afterwards. - Duplicates. Sift up stops on
heap[p] >= heap[i], so equal values never swap pointlessly. The tests include repeated values and negatives. - Index arithmetic. With 0-based storage the children are
2i + 1and2i + 2. The 1-based textbook formulas (2i,2i + 1, parenti / 2) silently break the heap if mixed in. - Sifting down to the wrong child. Always pick the larger child, and check
l < sizeandr < sizeseparately, because the last internal node may have only a left child. changeKeysemantics. The index refers to the internal array, so its effect depends on the layout. For the unsorted array the layout is insertion order, which is why the shared test inserts in decreasing order: both implementations then hold[10, 9, 7, 4].- Min vs max. A max-heap is a min-heap with every comparison flipped. In Java,
new PriorityQueue<>(Comparator.reverseOrder())gives a ready-made max-heap when you are allowed to use the library.
Related problems
- Implement Min Heap is the same structure with the comparisons reversed.
- Convert Min Heap to Max Heap is the bottom-up build from this article applied to an existing array.
- Check if an array represents a min heap checks the parent-child rule using the same index formulas.
- K-th largest element in an unsorted array. is a classic use of a heap.
- Find Median from Data Stream combines a max-heap and a min-heap.