Complete max-heap implementation from scratch — extract-max, increase-key, heap sort connection, and practical applications with full working code.
Max-Heap Definition
A max-heap is a complete binary tree where every node's value is greater than or equal to the values of its children. The root always contains the maximum element, accessible in O(1).
Valid Max-Heap: NOT a Max-Heap:
90 90
/ \ / \
70 80 70 80
/ \ / \ / \ / \
50 60 40 30 50 95 40 30
↑ child > parent (70)!
Complete Max-Heap Class Implementation
class MaxHeap:
def __init__(self):
self.heap = []
def __len__(self):
return len(self.heap)
def __bool__(self):
return len(self.heap) > 0
def _parent(self, i):
return (i - 1) // 2
def _left(self, i):
return 2 * i + 1
def _right(self, i):
return 2 * i + 2
def _swap(self, i, j):
self.heap[i], self.heap[j] = self.heap[j], self.heap[i]
def _sift_up(self, index):
"""Bubble element up to maintain max-heap property."""
while index > 0:
parent = self._parent(index)
if self.heap[index] > self.heap[parent]:
self._swap(index, parent)
index = parent
else:
break
def _sift_down(self, index):
"""Bubble element down to maintain max-heap property."""
n = len(self.heap)
while True:
largest = index
left = self._left(index)
right = self._right(index)
if left < n and self.heap[left] > self.heap[largest]:
largest = left
if right < n and self.heap[right] > self.heap[largest]:
largest = right
if largest == index:
break
self._swap(index, largest)
index = largest
def insert(self, value):
"""Insert a new element. O(log n)."""
self.heap.append(value)
self._sift_up(len(self.heap) - 1)
def peek(self):
"""Return maximum element without removing. O(1)."""
if not self.heap:
raise IndexError("Heap is empty")
return self.heap[0]
def extract_max(self):
"""Remove and return the maximum element. O(log n)."""
if not self.heap:
raise IndexError("Heap is empty")
max_val = self.heap[0]
last = self.heap.pop()
if self.heap:
self.heap[0] = last
self._sift_down(0)
return max_val
def increase_key(self, index, new_value):
"""Increase the value at index. O(log n)."""
if new_value < self.heap[index]:
raise ValueError(f"New value {new_value} is less than current {self.heap[index]}")
self.heap[index] = new_value
self._sift_up(index)
def delete(self, index):
"""Delete element at arbitrary index. O(log n)."""
# Increase to infinity, then extract max
self.heap[index] = float('inf')
self._sift_up(index)
self.extract_max()
@classmethod
def from_array(cls, arr):
"""Build max-heap from array in O(n) time."""
heap = cls()
heap.heap = arr[:]
n = len(heap.heap)
for i in range(n // 2 - 1, -1, -1):
heap._sift_down(i)
return heap
def __repr__(self):
return f"MaxHeap({self.heap})"
# Demonstration
h = MaxHeap()
for val in [15, 10, 20, 8, 25, 5, 30]:
h.insert(val)
print(f"Insert {val:2d}: {h.heap}")
print(f"\nPeek: {h.peek()}") # 30
print(f"Extract: {h.extract_max()}") # 30
print(f"Extract: {h.extract_max()}") # 25
print(f"Heap: {h.heap}") # [20, 10, 15, 8, 5]
Extract-max is the most important operation — it removes and returns the largest element while maintaining the heap structure.
| Heap | [90, 70, 80, 50, 60, 40, 30] |
| Step 1 | Save 90 (the max). Replace root with last element (30). |
| Step 2 | Sift down 30. |
| Swap | [80, 70, 30, 50, 60, 40] |
| Step 3 | Continue sifting 30 at index 2. |
| 30 < 40 | swap: [80, 70, 40, 50, 60, 30] |
| Step 4 | 30 is at index 5 (leaf). Done. |
| Result | Returned 90. New heap: [80, 70, 40, 50, 60, 30] |
Increase-Key Operation
Increase-key raises a node's value, potentially violating the heap property with its parent. Solution: sift up.
# Example: Increase index 4 from 60 to 95
heap = [80, 70, 40, 50, 60, 30]
# After update: [80, 70, 40, 50, 95, 30]
# 95 > parent 70 → swap: [80, 95, 40, 50, 70, 30]
# 95 > parent 80 → swap: [95, 80, 40, 50, 70, 30]
# 95 at root. Done.
This operation is crucial for priority queue update — when a task's priority increases, it needs to bubble up to the correct position.
Connection to Heap Sort
Heap sort uses a max-heap to sort an array in-place:
- Build a max-heap from the array — O(n)
- Repeatedly extract max: Swap root (max) with the last unsorted element, reduce heap size by 1, sift down the new root — O(n log n) total
def heap_sort(arr):
n = len(arr)
# Phase 1: Build max-heap in-place
for i in range(n // 2 - 1, -1, -1):
sift_down(arr, n, i)
# Phase 2: Extract elements one by one
for i in range(n - 1, 0, -1):
arr[0], arr[i] = arr[i], arr[0] # move max to end
sift_down(arr, i, 0) # restore heap on reduced array
def sift_down(arr, heap_size, index):
while True:
largest = index
left = 2 * index + 1
right = 2 * index + 2
if left < heap_size and arr[left] > arr[largest]:
largest = left
if right < heap_size and arr[right] > arr[largest]:
largest = right
if largest == index:
break
arr[index], arr[largest] = arr[largest], arr[index]
index = largest
# Test
arr = [4, 10, 3, 5, 1, 7, 8, 2]
heap_sort(arr)
print(arr) # [1, 2, 3, 4, 5, 7, 8, 10]
Heap Sort Dry Run
| Array | [4, 10, 3, 5, 1] |
| After build-heap | [10, 5, 3, 4, 1] |
| Extract 10: swap with last | [1, 5, 3, 4, | 10] |
| Sift down 1: | [5, 4, 3, 1, | 10] |
| Extract 5: swap with last | [1, 4, 3, | 5, 10] |
| Sift down 1: | [4, 1, 3, | 5, 10] |
| Extract 4: swap with last | [3, 1, | 4, 5, 10] |
| Sift down 3: | [3, 1, | 4, 5, 10] (already valid) |
| Extract 3: swap with last | [1, | 3, 4, 5, 10] |
| Final sorted | [1, 3, 4, 5, 10] ✓ |
Heap Sort Properties
| Property | Value |
|---|
| Time (all cases) | O(n log n) |
| Space | O(1) — in-place |
| Stable? | No (relative order of equal elements not preserved) |
| Adaptive? | No (same time regardless of input order) |
Heap sort is guaranteed O(n log n) (unlike quicksort's O(n²) worst case) and uses no extra space (unlike merge sort's O(n)).
C++ Complete Implementation
#include <vector>
#include <stdexcept>
#include <iostream>
using namespace std;
class MaxHeap {
vector<int> data;
void siftUp(int i) {
while (i > 0 && data[i] > data[(i-1)/2]) {
swap(data[i], data[(i-1)/2]);
i = (i-1)/2;
}
}
void siftDown(int i, int heapSize) {
while (2*i+1 < heapSize) {
int largest = i;
int l = 2*i+1, r = 2*i+2;
if (l < heapSize && data[l] > data[largest]) largest = l;
if (r < heapSize && data[r] > data[largest]) largest = r;
if (largest == i) break;
swap(data[i], data[largest]);
i = largest;
}
}
public:
void insert(int val) {
data.push_back(val);
siftUp(data.size()-1);
}
int extractMax() {
if (data.empty()) throw runtime_error("Empty heap");
int mx = data[0];
data[0] = data.back();
data.pop_back();
if (!data.empty()) siftDown(0, data.size());
return mx;
}
int peek() const { return data[0]; }
bool empty() const { return data.empty(); }
int size() const { return data.size(); }
void increaseKey(int idx, int val) {
if (val < data[idx]) throw runtime_error("Value too small");
data[idx] = val;
siftUp(idx);
}
// Build from array
MaxHeap(vector<int> arr) : data(move(arr)) {
for (int i = data.size()/2 - 1; i >= 0; i--)
siftDown(i, data.size());
}
MaxHeap() = default;
};
Practical Applications of Max-Heap
- Task scheduling: OS runs highest-priority process first (priority queue)
- Kth largest element: Maintain a min-heap of size K, or use max-heap with K extractions
- Merge K sorted arrays: Though min-heap is more natural for this
- Bandwidth management: Network routers serve highest-priority packets first
- Event simulation: Process events with highest priority/urgency first
- Median in data stream: Max-heap holds the lower half, min-heap the upper half
Key Takeaways
A max-heap keeps the maximum at the root, providing O(1) access and O(log n) extraction. The two core movements — sift up (after insert/increase-key) and sift down (after extract/decrease-key) — maintain the invariant. The heap sort algorithm leverages this to sort in-place in guaranteed O(n log n). Combined with O(n) build-heap, the max-heap is a versatile, efficient structure for priority-based processing.