Sitemap

Heap Data Structure: The Complete Guide

19 min readJun 2, 2026

--

Everything about heaps — what they are, how they work internally, min-heap vs max-heap, heapify algorithms, heap sort, priority queues, and how C++ STL handles all of it under the hood.

Every developer has used a heap without knowing it. When you call std::priority_queue in C++, heapq in Python, or PriorityQueue in Java — you're using a heap. When Dijkstra's algorithm finds the shortest path in GPS navigation, it's using a heap. When your operating system schedules which process runs next, it's using a heap. When a hospital triage system decides which patient gets treated first, the underlying data structure is conceptually a heap.

Yet most developers treat heaps as a black box — something they use but don’t understand deeply. This guide changes that. By the end, you’ll understand exactly how a heap works, why it works, how to implement one from scratch in C++, and when to reach for it in your own systems.

What Is a Heap?

A heap is a specialized binary tree that satisfies one crucial property: the heap property.

In a max-heap, every parent node is greater than or equal to both of its children. The largest element is always at the root.

In a min-heap, every parent node is less than or equal to both of its children. The smallest element is always at the root.

That’s the entire definition. Simple. But the implications are profound.

What a heap is NOT

Before going further, let’s clear up a common confusion. The heap data structure has nothing to do with “the heap” in memory management (where dynamic allocations like malloc and new come from). Same word, completely different concepts. When we say heap in this article, we always mean the data structure.

The two guarantees a heap provides

A heap gives you two things that make it uniquely valuable:

Guarantee 1: You can always find the maximum (or minimum) element in O(1) time. Just look at the root.

Guarantee 2: You can insert a new element or remove the root in O(log n) time.

No other data structure gives you both simultaneously. A sorted array gives you O(1) access to the max but O(n) insertion. An unsorted array gives you O(1) insertion but O(n) to find the max. A balanced BST gives you O(log n) for both, but with much higher constant factors and pointer overhead. The heap hits the sweet spot for specific use cases.

The Binary Heap — Structure and Representation

The most common implementation of a heap is the binary heap — a complete binary tree stored in an array.

Complete binary tree

A complete binary tree fills levels from left to right. Every level is fully filled except possibly the last, which is filled from left to right with no gaps. This completeness property is critical — it’s what allows us to store the tree in a flat array without pointers.

        90
/ \
75 85
/ \ / \
55 60 70 80
/ \
45 50

This is a valid max-heap. Every parent is larger than both children. The tree is complete.

Array representation — the elegant trick

Here’s the insight that makes heaps efficient in practice: a complete binary tree can be stored in an array where the structure is implicitly encoded by the indices.

For a 1-indexed array:

  • Root is at index 1
  • Left child of node at index i is at 2*i
  • Right child of node at index i is at 2*i + 1
  • Parent of node at index i is at i/2 (integer division)

For a 0-indexed array (what C++ STL uses):

  • Root is at index 0
  • Left child of node at index i is at 2*i + 1
  • Right child of node at index i is at 2*i + 2
  • Parent of node at index i is at (i-1) / 2

The tree above stored in a 1-indexed array:

Index: 1   2   3   4   5   6   7   8   9
Value: 90 75 85 55 60 70 80 45 50

No pointers. No nodes. Just an array. The parent-child relationships are computed mathematically, not stored. This makes heaps cache-friendly (data is contiguous in memory) and memory-efficient (no pointer overhead).

Min-Heap vs Max-Heap — In Depth

The heap property determines which extreme value sits at the root. Let’s explore both types with concrete examples.

Max-Heap

In a max-heap, the root always holds the largest value. Every subtree is also a max-heap — the property applies recursively throughout the tree.

Max-Heap:
100
/ \
90 80
/ \ / \
70 85 75 60
/ \
50 65

Array representation: [100, 90, 80, 70, 85, 75, 60, 50, 65]

Use max-heaps when you frequently need: the largest element, top-K largest elements, or any “maximum priority” scheduling.

Min-Heap

In a min-heap, the root always holds the smallest value.

Min-Heap:
10
/ \
20 15
/ \ / \
30 40 25 35
/ \
50 45

Array representation: [10, 20, 15, 30, 40, 25, 35, 50, 45]

Use min-heaps when you frequently need: the smallest element, shortest distances (Dijkstra), merge K sorted lists, or any “minimum priority” task.

Converting between them

Max-heap and min-heap are symmetric. Any max-heap algorithm becomes a min-heap algorithm by flipping all comparisons. In C++ STL, priority_queue<int> is a max-heap by default. To get a min-heap: priority_queue<int, vector<int>, greater<int>>.

Core Heap Operations — With Full Algorithm Walkthroughs

Operation 1: Heapify-Up (Bubble Up / Sift Up)

This is how insertion works. Add the new element at the end of the array (next available position in the tree), then “bubble it up” by repeatedly swapping with its parent until the heap property is restored.

Algorithm:

INSERT(heap, value):
1. Add value to end of array
2. i = last index
3. WHILE i > 0 AND heap[parent(i)] < heap[i]: // for max-heap
4. SWAP(heap[parent(i)], heap[i])
5. i = parent(i)

Example — Insert 95 into this max-heap:

Initial: [100, 90, 80, 70, 85, 75, 60, 50, 65]

Step 1 — Add 95 at end: [100, 90, 80, 70, 85, 75, 60, 50, 65, 95] 95 is at index 9. Parent is at index 4, value 85.

Step 2–95 > 85, swap: [100, 90, 80, 70, 95, 75, 60, 50, 65, 85] 95 is now at index 4. Parent is at index 1, value 90.

Step 3–95 > 90, swap: [100, 95, 80, 70, 90, 75, 60, 50, 65, 85] 95 is now at index 1. Parent is at index 0, value 100.

Step 4–95 < 100, stop. Heap property satisfied.

Final: [100, 95, 80, 70, 90, 75, 60, 50, 65, 85]

Time complexity: O(log n) — the element can bubble up at most the height of the tree, which is log n levels.

Operation 2: Heapify-Down (Bubble Down / Sift Down)

This is how deletion of the root works. Remove the root (the max or min element), move the last element to the root position, then “sift it down” by repeatedly swapping with the larger child (in a max-heap) until the heap property is restored.

Algorithm:

EXTRACT_MAX(heap):
1. max = heap[0]
2. heap[0] = heap[last]
3. Remove last element
4. i = 0
5. WHILE left_child(i) exists:
6. largest = i
7. IF heap[left_child(i)] > heap[largest]: largest = left_child(i)
8. IF right_child(i) exists AND heap[right_child(i)] > heap[largest]: largest = right_child(i)
9. IF largest == i: BREAK // heap property satisfied
10. SWAP(heap[i], heap[largest])
11. i = largest
12. RETURN max

Example — Extract max from [100, 95, 80, 70, 90, 75, 60]:

Step 1 — Remove 100 (the max), move 60 to root: [60, 95, 80, 70, 90, 75]

Step 2 — At index 0, children are 95 (index 1) and 80 (index 2). Largest child is 95. 60 < 95, swap: [95, 60, 80, 70, 90, 75]

Step 3–60 is now at index 1. Children are 70 (index 3) and 90 (index 4). Largest child is 90. 60 < 90, swap: [95, 90, 80, 70, 60, 75]

Step 4–60 is at index 4. Its child would be at index 9 — doesn’t exist. Stop.

Final max-heap: [95, 90, 80, 70, 60, 75]. Extracted value: 100.

Time complexity: O(log n) — the element can sink at most log n levels.

Operation 3: Build Heap (Heapify an array)

Given an unsorted array, convert it into a heap. The naive approach would insert elements one by one — O(n log n). But there’s a smarter way.

The insight: leaf nodes already satisfy the heap property trivially (they have no children). We only need to heapify the non-leaf nodes, and we do it bottom-up — starting from the last non-leaf node and working up to the root.

The last non-leaf node in a 0-indexed array of size n is at index (n/2) - 1.

Algorithm:

BUILD_HEAP(array, n):
FOR i FROM (n/2 - 1) DOWN TO 0:
HEAPIFY_DOWN(array, i, n)

Example — Build max-heap from [3, 1, 6, 5, 2, 4]:

n=6, start from index 2 (n/2–1 = 2).

Index 2, value 6. Children: none within range (indices 5,6–5 exists with value 4). 6 > 4, no swap.

Index 1, value 1. Children: 5 (index 3) and 2 (index 4). Largest child is 5. 1 < 5, swap: [3, 5, 6, 1, 2, 4] 1 is now at index 3. No children (would be indices 7,8). Stop.

Index 0, value 3. Children: 5 (index 1) and 6 (index 2). Largest child is 6. 3 < 6, swap: [6, 5, 3, 1, 2, 4] 3 is now at index 2. Children: 4 (index 5 — wait, index would be 5 and 6; 5 exists). 3 < 4, swap: [6, 5, 4, 1, 2, 3]

Final max-heap: [6, 5, 4, 1, 2, 3]

Time complexity: O(n) — This is a beautiful result. Even though each heapify-down is O(log n), most nodes are near the leaves and don’t travel far. Mathematical analysis shows the total work is O(n), not O(n log n).

Heap Sort — An O(n log n) In-Place Sorting Algorithm

Heap sort uses the heap structure to sort an array in-place with O(n log n) time and O(1) extra space.

Algorithm:

HEAP_SORT(array, n):
// Phase 1: Build a max-heap from the array — O(n)
BUILD_HEAP(array, n)

// Phase 2: Repeatedly extract the max and place it at the end — O(n log n)
FOR i FROM n-1 DOWN TO 1:
SWAP(array[0], array[i]) // Move current max to end
HEAPIFY_DOWN(array, 0, i) // Restore heap for remaining elements

Walkthrough with [4, 10, 3, 5, 1]:

Phase 1 — Build max-heap: [10, 5, 3, 4, 1]

Phase 2:

  • i=4: Swap root 10 with last: [1, 5, 3, 4, | 10]. Heapify [1,5,3,4] → [5, 4, 3, 1, | 10]
  • i=3: Swap root 5 with position 3: [1, 4, 3, | 5, 10]. Heapify [1,4,3] → [4, 1, 3, | 5, 10]
  • i=2: Swap root 4 with position 2: [3, 1, | 4, 5, 10]. Heapify [3,1] → [3, 1, | 4, 5, 10]
  • i=1: Swap root 3 with position 1: [1, | 3, 4, 5, 10]. Done.

Sorted: [1, 3, 4, 5, 10] ✓

How heap sort compares to other sorting algorithms

Heap sort is often overlooked in practice despite excellent theoretical properties. Here’s why:

Time complexity: O(n log n) in all cases — best, average, and worst. Unlike quicksort, there’s no O(n²) worst case.

Get Chandrachudsiddharth’s stories in your inbox

Join Medium for free to get updates from this writer.

Space complexity: O(1). True in-place sort. Merge sort needs O(n) extra space; quicksort needs O(log n) for the call stack.

The practical catch: Heap sort’s cache performance is poor. The array accesses jump around (parent at i/2, children at 2i and 2i+1), causing many cache misses. Quicksort’s access pattern is sequential, making it much more cache-friendly. In practice, quicksort on modern hardware typically outperforms heap sort despite the same Big-O.

When to use heap sort: When you need guaranteed O(n log n) (can’t tolerate quicksort’s worst case), O(1) extra space is required, and cache performance is less critical.

Priority Queue — The Primary Application of Heaps

A priority queue is an abstract data type where each element has a priority, and the element with the highest (or lowest) priority is always served first. A heap is the natural implementation.

Priority Queue operations:

  • insert(element, priority) — O(log n)
  • extract_max() or extract_min() — O(log n)
  • peek() — view the top element — O(1)
  • update_priority(element, new_priority) — O(log n) with index tracking

Priority queues appear everywhere in real systems. OS process schedulers maintain a priority queue of runnable processes. Network routers prioritize packets (voice over data, etc.). Dijkstra’s algorithm processes nodes by current shortest distance.

Heaps in C++ — Complete STL Guide

C++ provides heap functionality through both raw algorithms and the priority_queue container.

std::priority_queue — the container

#include <queue>
#include <vector>
#include <functional>
#include <iostream>
int main() {
// Max-heap (default) — largest element on top
std::priority_queue<int> maxHeap;

maxHeap.push(10);
maxHeap.push(30);
maxHeap.push(20);
maxHeap.push(5);
maxHeap.push(1);

std::cout << "Max: " << maxHeap.top() << "\n"; // 30
maxHeap.pop(); // removes 30
std::cout << "Next: " << maxHeap.top() << "\n"; // 20

// Size
std::cout << "Size: " << maxHeap.size() << "\n"; // 4

// Min-heap — smallest element on top
std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;

minHeap.push(10);
minHeap.push(30);
minHeap.push(20);

std::cout << "Min: " << minHeap.top() << "\n"; // 10

return 0;
}

Custom comparator — heaping by a struct field

#include <queue>
#include <string>
#include <iostream>
struct Task {
int priority;
std::string name;
};
// Custom comparator: higher priority number = higher priority task
struct ComparePriority {
bool operator()(const Task& a, const Task& b) {
// Returns true means a has LOWER priority than b
// (priority_queue puts the "greatest" on top)
return a.priority < b.priority;
}
};
int main() {
std::priority_queue<Task, std::vector<Task>, ComparePriority> taskQueue;

taskQueue.push({1, "Low priority task"});
taskQueue.push({10, "Critical task"});
taskQueue.push({5, "Medium task"});
taskQueue.push({8, "High priority task"});

while (!taskQueue.empty()) {
Task t = taskQueue.top();
taskQueue.pop();
std::cout << "Priority " << t.priority << ": " << t.name << "\n";
}
// Output:
// Priority 10: Critical task
// Priority 8: High priority task
// Priority 5: Medium task
// Priority 1: Low priority task

return 0;
}

Using a lambda as comparator

#include <queue>
#include <vector>
int main() {
// Min-heap using lambda
auto cmp = [](int a, int b) { return a > b; }; // greater = lower priority
std::priority_queue<int, std::vector<int>, decltype(cmp)> pq(cmp);

pq.push(5);
pq.push(1);
pq.push(8);
pq.push(3);

// Will pop: 1, 3, 5, 8 (ascending order)
while (!pq.empty()) {
std::cout << pq.top() << " ";
pq.pop();
}

return 0;
}

std::make_heap, std::push_heap, std::pop_heap — raw heap algorithms

These operate on any random-access container (typically std::vector) directly:

#include <algorithm>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

// Build a max-heap in-place — O(n)
std::make_heap(v.begin(), v.end());
// v is now a valid max-heap, e.g., [9, 6, 4, 1, 5, 3, 2, 1]

std::cout << "Max element: " << v.front() << "\n"; // 9

// Push a new element
v.push_back(10);
std::push_heap(v.begin(), v.end()); // Restores heap property — O(log n)
std::cout << "New max: " << v.front() << "\n"; // 10

// Pop the max element
std::pop_heap(v.begin(), v.end()); // Moves max to end — O(log n)
int maxVal = v.back();
v.pop_back();
std::cout << "Popped: " << maxVal << "\n"; // 10

// Heap sort using these primitives
std::vector<int> data = {5, 3, 8, 1, 9, 2};
std::make_heap(data.begin(), data.end());
std::sort_heap(data.begin(), data.end()); // Sorts in ascending order
// data is now [1, 2, 3, 5, 8, 9]

return 0;
}

Using std::greater for min-heap with raw algorithms

#include <algorithm>
#include <vector>
#include <functional>
int main() {
std::vector<int> v = {5, 3, 8, 1, 9};

// Build min-heap
std::make_heap(v.begin(), v.end(), std::greater<int>());
// v.front() == 1 (smallest)

// Push to min-heap
v.push_back(0);
std::push_heap(v.begin(), v.end(), std::greater<int>());
// v.front() == 0

// Pop min
std::pop_heap(v.begin(), v.end(), std::greater<int>());
v.pop_back();
// v.front() == 1

return 0;
}

Implementing a heap from scratch in C++

Understanding the STL is one thing; implementing a heap yourself cements the understanding. Here’s a complete, clean implementation:

#include <vector>
#include <stdexcept>
#include <iostream>
template <typename T, typename Compare = std::less<T>>
class Heap {
private:
std::vector<T> data;
Compare comp; // comp(a, b) = true means b should be higher in the heap

int parent(int i) { return (i - 1) / 2; }
int leftChild(int i) { return 2 * i + 1; }
int rightChild(int i) { return 2 * i + 2; }

void heapifyUp(int i) {
while (i > 0 && comp(data[parent(i)], data[i])) {
std::swap(data[i], data[parent(i)]);
i = parent(i);
}
}

void heapifyDown(int i) {
int size = data.size();
while (true) {
int largest = i;
int l = leftChild(i);
int r = rightChild(i);

if (l < size && comp(data[largest], data[l]))
largest = l;
if (r < size && comp(data[largest], data[r]))
largest = r;

if (largest == i) break;

std::swap(data[i], data[largest]);
i = largest;
}
}

public:
// Insert element — O(log n)
void push(const T& value) {
data.push_back(value);
heapifyUp(data.size() - 1);
}

// Remove top element — O(log n)
void pop() {
if (data.empty()) throw std::runtime_error("Heap is empty");
data[0] = data.back();
data.pop_back();
if (!data.empty()) heapifyDown(0);
}

// View top element — O(1)
const T& top() const {
if (data.empty()) throw std::runtime_error("Heap is empty");
return data[0];
}

// Build heap from existing vector — O(n)
void buildHeap(std::vector<T> input) {
data = std::move(input);
for (int i = data.size() / 2 - 1; i >= 0; i--)
heapifyDown(i);
}

bool empty() const { return data.empty(); }
int size() const { return data.size(); }
};
int main() {
// Max-heap (default with std::less)
Heap<int> maxHeap;
maxHeap.push(5);
maxHeap.push(10);
maxHeap.push(3);
maxHeap.push(8);

std::cout << "Max-heap top: " << maxHeap.top() << "\n"; // 10

// Min-heap (using std::greater)
Heap<int, std::greater<int>> minHeap;
minHeap.push(5);
minHeap.push(10);
minHeap.push(3);
minHeap.push(8);

std::cout << "Min-heap top: " << minHeap.top() << "\n"; // 3

// Build heap from vector
Heap<int> heapFromVector;
heapFromVector.buildHeap({9, 4, 7, 1, 8, 3, 6});
std::cout << "Built heap top: " << heapFromVector.top() << "\n"; // 9

return 0;
}

Classic Algorithms That Use Heaps

1. Dijkstra’s Shortest Path Algorithm

Dijkstra’s algorithm finds the shortest path from a source to all other vertices in a weighted graph. The min-heap is critical — it always gives us the unvisited vertex with the currently known shortest distance.

#include <queue>
#include <vector>
#include <climits>
using namespace std;typedef pair<int,int> pii; // {distance, vertex}vector<int> dijkstra(int src, int n, vector<vector<pii>>& adj) {
vector<int> dist(n, INT_MAX);
priority_queue<pii, vector<pii>, greater<pii>> minHeap; // min-heap by distance

dist[src] = 0;
minHeap.push({0, src});

while (!minHeap.empty()) {
auto [d, u] = minHeap.top();
minHeap.pop();

if (d > dist[u]) continue; // Already processed with shorter path

for (auto [w, v] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
minHeap.push({dist[v], v});
}
}
}

return dist;
}

Why a heap? Without it, finding the minimum-distance unvisited vertex takes O(V) per iteration — total O(V²). With a min-heap: O((V + E) log V). For sparse graphs (most real graphs), this is dramatically faster.

2. Merge K Sorted Lists/Arrays

Given K sorted arrays, merge them into one sorted array. The heap approach is elegant:

#include <queue>
#include <vector>
struct Element {
int val, arrayIdx, elementIdx;
bool operator>(const Element& other) const { return val > other.val; }
};
vector<int> mergeKSortedArrays(vector<vector<int>>& arrays) {
// Min-heap: always process the smallest current element
priority_queue<Element, vector<Element>, greater<Element>> minHeap;

// Initialize: push first element of each array
for (int i = 0; i < arrays.size(); i++) {
if (!arrays[i].empty())
minHeap.push({arrays[i][0], i, 0});
}

vector<int> result;

while (!minHeap.empty()) {
auto [val, ai, ei] = minHeap.top();
minHeap.pop();
result.push_back(val);

// Push next element from the same array
if (ei + 1 < arrays[ai].size())
minHeap.push({arrays[ai][ei + 1], ai, ei + 1});
}

return result;
}

Time complexity: O(N log K) where N is total elements and K is number of arrays. The heap size is at most K, so each push/pop is O(log K).

3. Find K Largest Elements

#include <queue>
#include <vector>
// Return k largest elements from an array
vector<int> kLargest(vector<int>& nums, int k) {
// Min-heap of size k — maintains k largest seen so far
priority_queue<int, vector<int>, greater<int>> minHeap;

for (int num : nums) {
minHeap.push(num);
if (minHeap.size() > k)
minHeap.pop(); // Remove the smallest among top k
}

vector<int> result;
while (!minHeap.empty()) {
result.push_back(minHeap.top());
minHeap.pop();
}
return result; // Will be in ascending order
}

Why a min-heap of size K? Counterintuitively, to find the K largest elements, we use a min-heap. We maintain a window of the K largest elements seen so far. When a new element is larger than the current minimum (the top of our min-heap), it replaces it. The heap never grows beyond K elements, so space is O(K) and time is O(N log K).

4. Kth Largest Element

A variation of the above — find exactly the Kth largest element:

int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> minHeap;

for (int num : nums) {
minHeap.push(num);
if (minHeap.size() > k)
minHeap.pop();
}

return minHeap.top(); // Kth largest
}

5. Median of a Data Stream

One of the most elegant heap problems. Maintain two heaps: a max-heap for the lower half of numbers, and a min-heap for the upper half. The median is always at the tops of these heaps.

class MedianFinder {
private:
priority_queue<int> lowerHalf; // max-heap
priority_queue<int, vector<int>, greater<int>> upperHalf; // min-heap

public:
void addNum(int num) {
lowerHalf.push(num);

// Balance: ensure lowerHalf.top() <= upperHalf.top()
if (!upperHalf.empty() && lowerHalf.top() > upperHalf.top()) {
upperHalf.push(lowerHalf.top());
lowerHalf.pop();
}

// Balance sizes: lowerHalf can have at most 1 more element
if (lowerHalf.size() > upperHalf.size() + 1) {
upperHalf.push(lowerHalf.top());
lowerHalf.pop();
} else if (upperHalf.size() > lowerHalf.size()) {
lowerHalf.push(upperHalf.top());
upperHalf.pop();
}
}

double findMedian() {
if (lowerHalf.size() > upperHalf.size())
return lowerHalf.top();
return (lowerHalf.top() + upperHalf.top()) / 2.0;
}
};

Why two heaps? To find the median, you need the middle element(s). Splitting the data into two halves — each maintained as a heap — gives O(log n) insertion and O(1) median access.

6. Prim’s Minimum Spanning Tree

#include <queue>
#include <vector>
#include <climits>
int primsMST(int n, vector<vector<pair<int,int>>>& adj) {
vector<bool> inMST(n, false);
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> minHeap;

minHeap.push({0, 0}); // {weight, vertex}
int totalCost = 0;

while (!minHeap.empty()) {
auto [w, u] = minHeap.top();
minHeap.pop();

if (inMST[u]) continue;
inMST[u] = true;
totalCost += w;

for (auto [weight, v] : adj[u]) {
if (!inMST[v])
minHeap.push({weight, v});
}
}

return totalCost;
}

Variations and Advanced Heap Types

D-ary Heap

Instead of two children per node, a D-ary heap has D children. The array index formulas become:

  • Children of node i: d*i + 1 to d*i + d
  • Parent of node i: (i - 1) / d

A higher branching factor reduces tree height (to log_d(n)) and thus speeds up extraction. But insertion may be slower since there are more children to compare. D-ary heaps are useful when insertions heavily outnumber extractions.

Fibonacci Heap

The Fibonacci heap is theoretically superior for decrease-key operations (changing a node’s priority), achieving O(1) amortized for decrease-key vs O(log n) for binary heaps. Dijkstra with a Fibonacci heap runs in O(E + V log V) vs O((E + V) log V) for a binary heap.

In practice, Fibonacci heaps have large constants and complex implementation, so binary heaps typically win in the real world. But they’re important in theoretical computer science.

Binomial Heap

A Binomial heap is a collection of binomial trees that supports efficient merge operations in O(log n). If you need to frequently merge two priority queues, a binomial heap outperforms a binary heap (which requires O(n) for merging).

Leftist Heap and Skew Heap

Both support merge in O(log n) and are used when mergeable priority queues are needed. Skew heaps sacrifice the structural guarantee of leftist heaps for simpler implementation.

Time and Space Complexity Summary

Operation Binary Heap D-ary Heap Fibonacci Heap (amortized) Build from array O(n) O(n) O(n) Insert O(log n) O(log_d n) O(1) Extract max/min O(log n) O(d * log_d n) O(log n) Peek max/min O(1) O(1) O(1) Decrease key O(log n) O(log_d n) O(1) Delete arbitrary O(log n) O(log_d n) O(log n) Merge two heaps O(n) O(n) O(1) Space O(n) O(n) O(n)

When to Use a Heap vs. Other Data Structures

The heap is the right choice when you have a recurring question: “What is the current minimum/maximum?” and the data is continuously changing.

Use a heap when you need repeated access to the extreme element (min or max), data is dynamic (insertions/deletions happening), you need top-K elements or K-way merging, and implementing graph algorithms like Dijkstra or Prim’s.

Use a sorted array when data is mostly static, you need O(1) access to any rank (not just the extreme), or you need binary search.

Use a balanced BST (like std::set or std::map in C++) when you need to find any element by value, need range queries, need both minimum and maximum, or need in-order traversal.

Use a queue (plain FIFO) when all elements have equal priority and order of arrival is what matters.

Common Mistakes and Gotchas

Mistake 1: Forgetting that std::priority_queue has no update_priority operation. If you need to update a node's priority (common in Dijkstra's), you have two options: use the "lazy deletion" approach (push the updated priority and ignore stale entries when you pop them), or implement your own indexed heap with a separate position map.

Mistake 2: Using std::priority_queue as a min-heap with greater<T> but forgetting to specify the container type. The correct declaration is priority_queue<int, vector<int>, greater<int>> — you must specify vector<int> as the second template argument before you can specify the comparator.

Mistake 3: Assuming pop() returns a value. In C++ STL, pop() discards the top element. To use the value, call top() first, store it, then call pop().

Mistake 4: Comparing heaps and BSTs incorrectly. A heap is not a sorted structure — it only guarantees the root is the maximum/minimum. If you need to iterate elements in sorted order, use a BST or sorted container.

Mistake 5: Modifying elements in a priority queue. Unlike std::set, you cannot modify elements inside a priority_queue. Doing so would corrupt the heap property. Always remove, modify, and re-insert.

Practical Example: Task Scheduler

Let’s put everything together with a realistic example — a task scheduler that processes jobs by priority:

#include <queue>
#include <string>
#include <iostream>
#include <chrono>
struct Job {
int priority;
int jobId;
std::string description;
std::chrono::steady_clock::time_point enqueueTime;

// Comparator: higher priority number = processed first
// Tie-break: earlier enqueue time goes first (FIFO within same priority)
bool operator<(const Job& other) const {
if (priority != other.priority)
return priority < other.priority; // higher priority wins
return enqueueTime > other.enqueueTime; // earlier time wins (note: reversed)
}
};
class TaskScheduler {
private:
std::priority_queue<Job> pq;
int jobCounter = 0;

public:
void enqueue(int priority, const std::string& desc) {
Job job;
job.priority = priority;
job.jobId = ++jobCounter;
job.description = desc;
job.enqueueTime = std::chrono::steady_clock::now();
pq.push(job);
std::cout << "Enqueued job #" << job.jobId
<< " [P=" << priority << "]: " << desc << "\n";
}

void processNext() {
if (pq.empty()) {
std::cout << "No jobs pending.\n";
return;
}
Job job = pq.top();
pq.pop();
std::cout << "Processing job #" << job.jobId
<< " [P=" << job.priority << "]: " << job.description << "\n";
}

void processAll() {
std::cout << "\n--- Processing all jobs ---\n";
while (!pq.empty()) processNext();
}

int pendingCount() const { return pq.size(); }
};
int main() {
TaskScheduler scheduler;

scheduler.enqueue(3, "Send welcome email");
scheduler.enqueue(10, "Process payment — CRITICAL");
scheduler.enqueue(7, "Update user profile");
scheduler.enqueue(10, "Verify fraud alert — CRITICAL");
scheduler.enqueue(1, "Generate monthly report");
scheduler.enqueue(5, "Resize uploaded images");

std::cout << "\nPending jobs: " << scheduler.pendingCount() << "\n";
scheduler.processAll();

return 0;
}
/* Output:
Processing all jobs:
Processing job #2 [P=10]: Process payment — CRITICAL
Processing job #4 [P=10]: Verify fraud alert — CRITICAL
Processing job #3 [P=7]: Update user profile
Processing job #6 [P=5]: Resize uploaded images
Processing job #1 [P=3]: Send welcome email
Processing job #5 [P=1]: Generate monthly report
*/

The Big Picture

The heap data structure embodies a beautiful design principle: don’t compute what you don’t need. You don’t need a fully sorted structure — you just need to know the current maximum or minimum. The heap gives you exactly that, and nothing more, at the minimum possible cost.

Every time you use Dijkstra’s algorithm, every time you process elements by priority, every time you stream data and need the running median, you’re relying on this structure. The array trick — encoding a tree’s parent-child relationships in indices — is one of those ideas that feels obvious once you see it and impossible to have come up with yourself.

Implement a heap from scratch at least once. Work through the heapify operations by hand with a concrete array. Once the bubble-up and sift-down motions are in your fingers, the structure stops being abstract and becomes a natural tool.

Then, in the real world, use std::priority_queue. It's fast, well-tested, and already in your standard library. But now you know exactly what it's doing — and that's the difference between using a tool and understanding it.

Next steps: implement Dijkstra’s using a priority queue on a real graph, then try the “Kth Largest Element in a Stream” problem on LeetCode (#703). Those two exercises will cement every concept in this article.

--

--