Today we'll talk about heaps which are similar to trees where each parent node value is greater than its childrens. There are two types of heaps maxHeaps and minHeaps as the name suggests max heap goes from highest to lowest value and min heap goes from lowest to greatest.
The heap structure is an abstract concept, their implementation relies on a simple array. Now you might ask, if we rely on arrays how do we know which one is the parent and which ones are their child's.
Well it's an array where order matters, the root is obviously the first node and it's child are the following ones.
For simplicity sake we will take binary heaps so that we have at most 2 childs, if we are looking for the childs of the root node we would look at index 1 and 2. Ok now what if we want to look for the childs of node at index 42 ?
Well we need a way to find the childrens solely based on the parent's index (where n is the index):
Now what about the parent: (n - 1) / 2 (where n is the index) So if we look at the parent of index 1 : (1 - 1) / 2 = 0 If we now look a the parent of index 6 : (6 - 1) / 2 = 2.5
For this exact use case we need to floor the result so that we always get the right index : Math.floor((index - 1) / 2)
The Heap class represents our custom data structure which is actually simply an array.
class maxBinaryHeap {
constructor() {
this.values = [];
}
}
Insert a new value into the heap.
function insert(value) {
this.values.push(value);
let index = this.values.length - 1;
const element = this.values[index];
while (true) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this.values[parentIndex];
if (parent >= element) break;
this.values[parentIndex] = element;
this.values[index] = parent;
index = parentIndex;
}
}
const heap = [45, 36, 38, 24, 5];
heap.insert(12); // [45, 36, 38, 24, 5, 12]
heap.insert(56); // [56, 36, 45, 24, 5, 12, 38]
Explanation: Here we need what we call bubbling, we essentially add the new value to the end of the array before bubbling the value up (comparing and replacing with its parent as long as the parent is smaller that the value):
ExtractMax or (extractMin depending on the heap's type). Which as the name suggests is used to pop the head of the tree before replacing it with the biggest value, for this we will need bubbling again.
function extractMax() {
let index = 0;
const head = this.values[index];
const end = this.values.pop();
if (!this.values.length) return head;
const length = this.values.length;
this.values[0] = end;
const element = this.values[index];
while (true) {
const leftIndex = 2 * index + 1;
const rightIndex = 2 * index + 2;
let left = null;
let right = null;
let swap = null;
if (leftIndex < length) {
left = this.values[leftIndex];
if (left > element) {
swap = leftIndex;
}
}
if (rightIndex < length) {
right = this.values[rightIndex];
if (
(swap === null && right > element) ||
(swap !== null && right > left)
) {
swap = rightIndex;
}
}
if (swap === null) break;
this.values[index] = this.values[swap];
this.values[swap] = element;
index = swap;
}
return head;
}
Explanation:
Let break the result down:
const heap = [56, 36, 45, 24, 5, 12];
heap.extractMax();
// 1. [56, 36, 45, 24, 5]
// 2. [12, 36, 45, 24, 5]
// 3. [45, 36, 12, 24, 5]
// 4. [45, 36, 24, 12, 5]
// Return 56
So here we have it our head is successfully removed and returned and our tree is successfully reordered.
Now that we know what a heap looks like when should we use it? Well heaps are a great data structure for real world scenarios that involves prioritizing.
Let's see from our heap how we can take it one step further and build a priority queue from it:
Same as before we have our priority queue class we a simple array:
class PriorityQueue {
constructor() {
this.values = [];
}
}
Now to handle more than a simple value we have nodes which hold a value and a priority which could hold a value from 1 to 5 where 1 is the highest for example.
class Node {
constructor(value, priority) {
this.value = value;
this.priority = priority;
}
}
Same function as before but now instead of comparing the element itself we compare by priority:
function insert(value, priority) {
const newNode = new Node(value, priority);
this.values.push(newNode);
let index = this.values.length - 1;
const element = this.values[index];
while (true) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this.values[parentIndex];
if (parent.priority >= element.priority) break;
this.values[parentIndex] = element;
this.values[index] = parent;
index = parentIndex;
}
}
const heap = [45, 36, 38, 24, 5];
heap.insert(12); // [45, 36, 38, 24, 5, 12]
heap.insert(56); // [56, 36, 45, 24, 5, 12, 38]
Same here, we compare by the priority property instead of the value itself:
function extractMax() {
let index = 0;
const head = this.values[index];
const end = this.values.pop();
if (!this.values.length) return head;
const length = this.values.length;
const element = this.values[index];
this.values[index] = end;
while (true) {
const leftIndex = 2 * index + 1;
const rightIndex = 2 * index + 2;
let left = null;
let right = null;
let swap = null;
if (leftIndex < length) {
left = this.values[leftIndex];
if (left.priority > element.priority) {
swap = leftIndex;
}
}
if (rightIndex < length) {
right = this.values[rightIndex];
if (
(swap === null && right.priority > element.priority) ||
(swap !== null && right.priority > left.priority)
) {
swap = rightIndex;
}
}
if (swap === null) break;
this.values[index] = this.values[swap];
this.values[swap] = element;
index = swap;
}
return head;
}
Let's say we are building a todo list where each element has a priority ranging from 1 to 5 with 5 being the highest priority.
const priority_queue = new PriorityQueue();
priority_queue.insert("work", 5);
priority_queue.insert("sport", 4);
priority_queue.insert("laundry", 5);
const firstTodo = priority_queue.extractMax();
// {value: "work", priority: 5}
const secondTodo = priority_queue.extractMax();
// {value: "laundry", priority: 5}
// "sport"
priority_queue.insert("grocery", 5);
// "grocery"
// "sport"
So there we have a fully functional Todo list thanks to our priority queue data structure. Note that the logic here relies on the highest priority but it could also be the lowest or any other value you choose.
Now that we know when do use it, why do we use it? Well because they are really fast at insertion and deletion and when I say really fast I mean bigO(log N) which is really performant as constant time is almost impossible unless sacrifying space complexity. Why? Because when we reorder the tree (or heapify) we only have to check once for every level. If we for example take a binary heap with 16 elements we only have 4 comparison to make, 1 for every level.
So that's it for heaps, I hope you now understand when and how to use them, if you have any questions or clarifications please leave a comment I might learn something from you as well!
Happy coding!
Honestly i am no one, i've just been coding for 3 years now and i like to document every solutions to every problem i encounter. Extracting as much code snippets and tutorials i can so that if i ever need it again i just have to pop back here and get a quick refresher.
Feel free to me through this learning journey by providing any feedback and if you wanna support me: