AlgoMaster Logo

Introduction to Heaps

Ashish

Ashish Pratap Singh

Heaps are a a specialized tree-based data structure designed to efficiently retrieve the highest priority element at any time.

This "priority" can mean the largest value, the smallest value, or the item with the earliest deadline, making heaps exceptionally versatile.

Heaps guarantee O(log n) time for insertion and removal and O(1) time for accessing the highest priority element.

Because of these properties, heaps are widely used in real-world systems including:

  • priority based task scheduling in operating systems
  • and shortest path algorithms like Dijkstra

In this chapter, I’ll cover:

  • What a heap is and how it works
  • Difference types of heaps
  • how to implement heap in code
  • Core heap operations and their complexities

What is a Heap?

So, what exactly is a heap?

A heap is a special kind of binary tree optimized for fast access to the highest-priority element.

503010204015

It follows two key rules:

1. Complete Binary Tree

  • A heap is always a complete binary tree.
  • That means every level of the tree is completely filled, except possibly the last.
  • And in the last level, nodes are filled from left to right with no gaps.

This is what keeps the tree balanced and ensures operations like insertion and deletion are efficient.

2. Heap Property

Every heap must also satisfy the heap property, which comes in two flavors:

Max Heap
503010204015
  • Every parent node is greater than or equal to its children.
  • The root node is always the largest element in the heap.
  • Used when you need fast access to the maximum value.
Min Heap
5103040152520
  • Every parent node is smaller than or equal to its children.
  • The root node is always the smallest element in the heap.
  • Useful when you want quick access to the minimum value.

Representing a Heap

Even though a heap is conceptually a binary tree, we don’t usually store it using nodes and pointers like a typical tree. Instead, the most common and efficient way to represent a heap is with a simple array.

Because a heap is a complete binary tree (filled level by level, left to right), it maps perfectly into an array without wasting space.

  • For a node at index i:
    • The left child is at 2 * i + 1
    • The right child is at 2 * i + 2
    • The parent is at (i - 1) / 2

This mapping allows us to navigate the heap without storing extra pointers, which makes the implementation simpler and faster.

Here:

  • The heap array stores the values.
  • size keeps track of how many elements are in the heap.
  • And with those helper functions, we can easily move between parent and child nodes.

Quick Example

Consider a max heap stored as:

5030151020816
  • At the top, index 0 is our root: 50.
  • Its children? We check 2*0+1 (index 1) and 2*0+2 (index 2). And there they are: 30 and 20.
  • Let's try another one. Take 30, which is at index 1.
  • Its children should be at 2*1+1 (index 3) and 2*1+2 (index 4). And , that's 15 and 10.

This is why heaps are implemented using arrays — the structure lines up naturally and keeps the operations fast and simple.

Common Heap Operations

Now that you know how heaps are structured, let’s look at the core operation you’ll use in practice.

For the examples we will use a max-heap. The logic for a Min-Heap is identical; you just reverse the comparison operations.

1. Peek

This is the simplest and fastest operation.

  • The peek operation simply returns the root element without removing it.
  • In a max-heap, it’s the max value and it’s always at the root of the three which corresponds to the first element in our heap array atindex 0
  • The time complexity is O(1) since it’s a direct lookup.

2. Insert

When we add a new element, we must satisfy both heap properties:

  1. Complete Tree Property: The tree must be filled from left to right, with no gaps.
  2. and Heap Property: Every parent must be greater that its children (for a max-heap).

It's easiest to satisfy the Complete Tree property first. We just add the new element to the very last available spot in the tree, which is simply the end of our array.

This, however, will almost certainly break the Heap Property. The new element might be larger than its parent. To fix this, we "bubble up" (or "percolate up") the new element.

We compare it to its parent; if it's larger, we swap them. We repeat this process—compare, swap, move up—until the new element finds its rightful place, either by being smaller than its new parent or by becoming the new root.

Here’s what it looks like in code:

  1. Add the new value to the end of the heap list.
  2. Get the index of this new element (i = heap.size() - 1).
  3. While the element is not the root (i > 0):
    1. Find its parent's index (parent = (i - 1) / 2).
    2. If the element is greater than its parent, swap them.
    3. Update i to be the parent's index and continue the loop.
    4. If the element is less than or equal to its parent, the heap property is restored. Stop the loop.

Time Complexity is O(log n) because the height of a complete binary tree with n nodes is log_2(n). In the worst case, the new element must travel all the way from a leaf to the root. This path is, by definition, the height of the tree.

3. Extract Max: Remove the Top Element

This is the most common heap operation. It removes and returns the root element. Like insert, this operation must also preserve both heap properties.

Removing the root (index 0) is easy, but it leaves a hole at the top. This breaks the Complete Tree property.

To fix this, we use a clever trick:

  1. We take the last element from the array (the right-most leaf) and move it into the root's empty spot.
  2. This restores the Complete Tree property immediately. We no longer have a hole.
  3. But now, the Heap Property is almost certainly broken. The element we just moved to the root is likely smaller than its new children.

To fix this, we "sift down" (or "heapify down") the new root. We compare it to its children. If it's smaller than either of them, we swap it with the larger of its two children (to ensure the largest value moves up). We repeat this process—compare, swap with largest child, move down—until the element finds its correct position (i.e., it's larger than both of its children, or it becomes a leaf).

Here’s how it looks in code:

  1. If the heap is empty, throw an exception.
  2. Store the root element (heap.get(0)) in a variable to return later.
  3. Remove the last element from the heap and store it.
  4. If the heap is now empty (we just removed the only element), return the stored root.
  5. If not, place the (previously last) element at the root (heap.set(0, lastElement)).
  6. Call heapifyDown(0) to sift the new root down to its correct position.

Time Complexity: is O(log n). Just like insert, the operation follows a path from the root to a leaf. The length of this path is the tree's height, which is O(log n).

4. Build Heap (Heapify an Array)

Sometimes, you are given an entire unsorted array and need to convert it into a valid heap.

You could start with an empty heap and call insert() for each of the $n$ elements in the array. Since each insert is O(log n), this would take O(n log n) time. We can do much better!

We know that all leaf nodes (the entire second half of the array) are, by definition, already valid heaps of size 1. They have no children, so the heap property is trivially true.

The real work is with the non-leaf nodes (the first half of the array). If we can ensure that every non-leaf node satisfies the heap property relative to its children, the whole tree will become a valid heap. So, we iterate backwards from the last non-leaf node up to the root (index 0). For each of these nodes, we call heapifyDown(). This "sifts down" the element, allowing larger children to move up and restoring the local heap property. Because we start from the bottom, by the time we heapifyDown a node, we are guaranteed that its children are already valid heap roots themselves.

Here is how it works:

  1. Copy all elements from the input array into our heap list.
  2. Find the index of the last non-leaf node. This is (heap.size() / 2) - 1.
  3. Iterate backwards from this index down to 0 (for (int i = ...; i >= 0; i--)).
  4. In each iteration, call heapifyDown(i).
  5. When the loop finishes, the entire array is a valid heap.

Time Complexity is O(n).

Why O(n) and not O(n log n)?

This is a classic analysis. It looks like O(n log n) because we call heapifyDown (an O(log n) operation) n/2 times. But this is a loose upper bound.

  • A tighter analysis shows that most of the work is cheap.
  • The nodes at the bottom (e.g., height 1) only sift down 1 level. There are n/4 such nodes.
  • The nodes at height 2 only sift down 2 levels. There are n/8 such nodes.
  • The root (height log n) sifts down log n levels, but there is only 1 root.
  • The total work is the sum: W = sum_{h=0}^{log n} frac{n}{2^{h+1}} \cdot O(h). This mathematical series converges to O(n).

You are doing a lot of cheap work on the many nodes at the bottom and a small amount of expensive work on the few nodes at the top. The total cost is dominated by the linear number of nodes, not the logarithmic height.