Heaps and Heap-sort
December 28, 2016
What does a heap do?
In this tutorial, we’ll learn about heaps, a very interesting data structure. Heaps are a data-structure that efficiently support the following operations
- Insert / Push: Insert a new element to a set
- Delete-min / Pop: Find and remove the smallest element in the set
Note that the operations can be performed multiple times and in any order. (Also, note that actual implementations also support operation top, which returns the smallest element, but does not remove it from the heap).
The following is a sample interaction we would want with a heap.
code | set of elements | result
-----------------|-----------------|--------
heap = Heap() | {} |
heap.insert(5) | {5} |
heap.insert(2) | {5, 2} |
heap.insert(10) | {5, 2, 10} |
heap.pop() | {5, 10} | 2
heap.pop() | {10} | 5
heap.insert(7) | {10, 7} |
heap.insert(12) | {10, 7, 12} |
heap.pop() | {10, 12} | 7
heap.pop() | {12} | 10
It doesn’t matter how the set is stored. What matters is that we always get the correct result. A heap can perform both the operations above in O($\log n$) time per operation, where $n$ is the number of elements currently in the set.
Max-heaps
In this article, we are talking about min-heaps. But similarly, we can also define max-heaps which remove the largest element instead of the smallest element.
Illustration
Python
import heapq
heap = []
heapq.heappush(heap, 5) # set = {5}
heapq.heappush(heap, 2) # set = {5, 2}
heapq.heappush(heap, 10) # set = {5, 2, 10}
print(list(heap))
heapq.heappop(heap) # result = 2. set = {5, 10}
heapq.heappop(heap) # result = 5. set = {10}
print(list(heap))
heapq.heappush(heap, 7) # set = {10, 7}
heapq.heappush(heap, 12) # set = {10, 7, 12}
print(list(heap))
heapq.heappop(heap) # result = 7. set = {10, 12}
heapq.heappop(heap) # result = 10. set = {12}
print(list(heap))
C++
In C++, priority_queue
can be used to implement heaps. The following code shows its application.
#include <bits/stdc++.h>
using namespace std;
int main()
{
priority_queue<int, vector<int>, greater<int> >pq;
pq.push(5);
pq.push(2);
pq.push(10);
cout << pq.top() << endl;
pq.pop();
cout << pq.top() << endl;
pq.pop();
pq.push(7);
pq.push(12);
cout << pq.top() << endl;
pq.pop();
cout << pq.top() << endl;
pq.pop();
return 0;
}
By default, priority_queue
is implemented as a max heap in cpp. To use it as a min heap in the above code, we have declared it as the following:
priority_queue<datatype, vector<datatype>, greater<datatype> >pq;
Here, datatype can be any datatype such as int, float, pair<double, double > or even user defined datatypes.
Can we use a list / array instead?
Note that if we use a list to perform these operations, then the first operation (insert) can be done in O(1), but the second operation (finding the removing the minimum) takes O($n$). If instead we always maintain a sorted list, then finding and removing the minimum takes O(1), but inserting a new element takes O($n$).
So, if we have equal number of operation 1 and operation 2, then lists take time O($qn$), whereas the heap takes O($q \log n$), where $q$ is the total number of operations.
Heap-sort
Before we get into heaps, let’s see how heap-sort would work assuming we have a heap.
define heapsort(array):
heap = empty heap
for element in array:
heap.insert(element)
sorted = empty list
while heap is not empty
sorted.append(heap.pop())
return sorted
That is, we do $n$ insert operations, and then $n$ pop operations. Since each operation is O($\log n$), heap sort takes O($n \log n$) time total.
Other applications of heaps
Heap-sort is only one application of a heap. There are many other important applications of heaps. Most notably, these include (a) Dijkstra’s shortest-path algorithm for weighted graphs, (b) Prim’s Minimum Spanning Tree algorithm, (c) as priority queues (data structures that support the above two heap operations).
How do heaps work?
The heap data structure is a binary tree (each internal node has two children). Also, it is balanced, which means that new nodes are added layer by layer, left to right (see image below).
Example of a min-heap
Lastly, note that some of nodes will have 0 children (these are called leaves), and it is possible for one of the nodes to have exactly 1 child (node with value 46 in the image above).
Heap property
The heap data structure’s efficiency comes because of maintaining the heap property. The property is: for every node, the value at the parent is less than or equal to the value at the children. For example, in the image above, 9 is smaller than both it’s children, 26 and 11. Similarly, 18 is smaller than both it’s children, 30 and 19.
Because of the heap property, the minimum value in the heap is always at the root node.
How are heaps stored?
Although heaps nodes form a binary tree, it is in-fact possible to implement heaps efficiently while storing all elements in an array. The elements in the array would be listed layer-by-layer, left to right. For example, the array corresponding to the above heap is:
3 9 4 26 11 18 20 35 46 12 15 30 19 26 24 71 80 52
Assuming that array indexes begin at 1, the children of position $p$ are at 2p
and 2p+1
. The parent of position $p$ is at position floor(p/2)
.
Insert operation
Now we’ll see how to perform the insert and delete-min operations in O($\log n$) time while maintaining the heap property. We’ll start with the insert operation. Here’s the pseudocode
define insert(element)
insert element to the first empty position in the heap
while element's parent is larger than element:
swap(element, parent)
Let’s see an example. In the example, we insert 5 as a new leaf. To resolve this, we swap 5 with 46, then with 26, then with 9, and then stop (since 3 is smaller than 5).
Inserting 5 to the heap. First, we just add a new leaf with value 5. Then, keep swapping up (with parent), while parent is > 5.
At any point, the only possible place where the heap property is violated is between 5 and it’s current parent, which is resolved when all the upward-swaps are completed.
No other new violations are introduced between other nodes. For example, when we swap 5 and 46, 5 and 52 cannot violate the heap property since 5 is smaller than 46, and 46 is smaller than 52.
Delete-min operation
Finally, here’s the pseudocode for the delete-min operation
define pop()
top = value at root
remove last node of the heap and store value at the root
node = root
while either of node's children is smaller than node:
swap(node, whichever child is smaller)
Let’s see an example below. Suppose we do the delete-min operation on our heap. The minimum value 3 is at the root. We need to remove this value which reduces the total number of nodes in the heap. So we remove the last element (46) and put the value at position 3. Because of this, the heap property might be violated between 46 and it’s children.
To fix this, swap 46 with whichever child is smaller. The first swap is 46 and 4. Then 46 and 18. Then 46 and 19. At any point, the only possible place where the heap property is violated is between 46 and it’s current children, which is resolved when all the downward-swaps are completed.
Popping from the heap. Remove the last node (46) and put it at the root (3). While there is a child which is smaller, swap with the smaller of the two children. Return the “original” root (3).
Conclusion
Heaps are a powerful and important data structure. In this tutorial, we saw how they support the insert and delete-min operations in O($\log n$) time per operation. They achieve this by storing all the elements in a binary tree of height $\log n$ which always maintains the heap property.
In the next tutorial, we’ll implement the heap data-structure from scratch and use it to perform heap-sort.