Heaps are tree-based data structures that maintain a specific ordering property. This guide covers heap operations, priority queues, and common heap problems.
What is a Heap? #
A heap is a complete binary tree where each node satisfies the heap property:
- Max Heap: Parent ≥ children
- Min Heap: Parent ≤ children
Max Heap: Min Heap:
50 10
/ \ / \
30 40 20 30
/ \ / / \ /
10 20 35 40 50 35Array Representation #
Heaps are typically stored in arrays.
// Array: [50, 30, 40, 10, 20, 35]
// Indices: 0 1 2 3 4 5
// For node at index i:
// Left child: 2 * i + 1
// Right child: 2 * i + 2
// Parent: Math.floor((i - 1) / 2)
function leftChild(i) {
return 2 * i + 1;
}
function rightChild(i) {
return 2 * i + 2;
}
function parent(i) {
return Math.floor((i - 1) / 2);
}Min Heap Implementation #
class MinHeap {
constructor() {
this.heap = [];
}
size() {
return this.heap.length;
}
peek() {
return this.heap[0];
}
insert(value) {
this.heap.push(value);
this.bubbleUp(this.heap.length - 1);
}
bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex] <= this.heap[index]) {
break;
}
[this.heap[parentIndex], this.heap[index]] =
[this.heap[index], this.heap[parentIndex]];
index = parentIndex;
}
}
extractMin() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const min = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return min;
}
bubbleDown(index) {
while (true) {
const leftIdx = 2 * index + 1;
const rightIdx = 2 * index + 2;
let smallest = index;
if (leftIdx < this.heap.length &&
this.heap[leftIdx] < this.heap[smallest]) {
smallest = leftIdx;
}
if (rightIdx < this.heap.length &&
this.heap[rightIdx] < this.heap[smallest]) {
smallest = rightIdx;
}
if (smallest === index) break;
[this.heap[index], this.heap[smallest]] =
[this.heap[smallest], this.heap[index]];
index = smallest;
}
}
remove(value) {
const index = this.heap.indexOf(value);
if (index === -1) return false;
if (index === this.heap.length - 1) {
this.heap.pop();
return true;
}
this.heap[index] = this.heap.pop();
this.bubbleDown(index);
this.bubbleUp(index);
return true;
}
}
// Usage
const heap = new MinHeap();
heap.insert(5);
heap.insert(3);
heap.insert(7);
heap.insert(1);
console.log(heap.extractMin()); // 1
console.log(heap.extractMin()); // 3
console.log(heap.peek()); // 5
Max Heap Implementation #
class MaxHeap {
constructor() {
this.heap = [];
}
insert(value) {
this.heap.push(value);
this.bubbleUp(this.heap.length - 1);
}
bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex] >= this.heap[index]) {
break;
}
[this.heap[parentIndex], this.heap[index]] =
[this.heap[index], this.heap[parentIndex]];
index = parentIndex;
}
}
extractMax() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const max = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return max;
}
bubbleDown(index) {
while (true) {
const leftIdx = 2 * index + 1;
const rightIdx = 2 * index + 2;
let largest = index;
if (leftIdx < this.heap.length &&
this.heap[leftIdx] > this.heap[largest]) {
largest = leftIdx;
}
if (rightIdx < this.heap.length &&
this.heap[rightIdx] > this.heap[largest]) {
largest = rightIdx;
}
if (largest === index) break;
[this.heap[index], this.heap[largest]] =
[this.heap[largest], this.heap[index]];
index = largest;
}
}
}Priority Queue #
Priority queue using heap.
class PriorityQueue {
constructor(comparator = (a, b) => a - b) {
this.heap = [];
this.comparator = comparator;
}
size() {
return this.heap.length;
}
isEmpty() {
return this.heap.length === 0;
}
peek() {
return this.heap[0];
}
enqueue(value) {
this.heap.push(value);
this.bubbleUp(this.heap.length - 1);
}
dequeue() {
if (this.isEmpty()) return null;
if (this.size() === 1) return this.heap.pop();
const top = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return top;
}
bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.comparator(this.heap[parentIndex], this.heap[index]) <= 0) {
break;
}
[this.heap[parentIndex], this.heap[index]] =
[this.heap[index], this.heap[parentIndex]];
index = parentIndex;
}
}
bubbleDown(index) {
while (true) {
const leftIdx = 2 * index + 1;
const rightIdx = 2 * index + 2;
let smallest = index;
if (leftIdx < this.heap.length &&
this.comparator(this.heap[leftIdx], this.heap[smallest]) < 0) {
smallest = leftIdx;
}
if (rightIdx < this.heap.length &&
this.comparator(this.heap[rightIdx], this.heap[smallest]) < 0) {
smallest = rightIdx;
}
if (smallest === index) break;
[this.heap[index], this.heap[smallest]] =
[this.heap[smallest], this.heap[index]];
index = smallest;
}
}
}
// Usage with objects
const pq = new PriorityQueue((a, b) => a.priority - b.priority);
pq.enqueue({ task: 'Low priority', priority: 3 });
pq.enqueue({ task: 'High priority', priority: 1 });
pq.enqueue({ task: 'Medium priority', priority: 2 });
console.log(pq.dequeue()); // { task: 'High priority', priority: 1 }
console.log(pq.dequeue()); // { task: 'Medium priority', priority: 2 }
Heapify #
Convert array to heap in O(n).
function heapify(arr) {
// Start from last non-leaf node
const start = Math.floor(arr.length / 2) - 1;
for (let i = start; i >= 0; i--) {
bubbleDown(arr, i);
}
return arr;
}
function bubbleDown(arr, index) {
const length = arr.length;
while (true) {
const leftIdx = 2 * index + 1;
const rightIdx = 2 * index + 2;
let largest = index;
if (leftIdx < length && arr[leftIdx] > arr[largest]) {
largest = leftIdx;
}
if (rightIdx < length && arr[rightIdx] > arr[largest]) {
largest = rightIdx;
}
if (largest === index) break;
[arr[index], arr[largest]] = [arr[largest], arr[index]];
index = largest;
}
}
const arr = [3, 9, 2, 1, 4, 5];
heapify(arr);
console.log(arr); // [9, 4, 5, 1, 3, 2] (max heap)
Heap Sort #
function heapSort(arr) {
// Build max heap
heapify(arr);
// Extract elements
for (let i = arr.length - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
bubbleDownWithSize(arr, 0, i);
}
return arr;
}
function bubbleDownWithSize(arr, index, size) {
while (true) {
const leftIdx = 2 * index + 1;
const rightIdx = 2 * index + 2;
let largest = index;
if (leftIdx < size && arr[leftIdx] > arr[largest]) {
largest = leftIdx;
}
if (rightIdx < size && arr[rightIdx] > arr[largest]) {
largest = rightIdx;
}
if (largest === index) break;
[arr[index], arr[largest]] = [arr[largest], arr[index]];
index = largest;
}
}
const arr = [3, 9, 2, 1, 4, 5];
heapSort(arr);
console.log(arr); // [1, 2, 3, 4, 5, 9]
Classic Problems #
K Largest Elements #
function findKLargest(nums, k) {
const minHeap = new MinHeap();
for (const num of nums) {
minHeap.insert(num);
if (minHeap.size() > k) {
minHeap.extractMin();
}
}
return minHeap.heap;
}
console.log(findKLargest([3, 2, 1, 5, 6, 4], 2)); // [5, 6]
K Smallest Elements #
function findKSmallest(nums, k) {
const maxHeap = new MaxHeap();
for (const num of nums) {
maxHeap.insert(num);
if (maxHeap.heap.length > k) {
maxHeap.extractMax();
}
}
return maxHeap.heap;
}
console.log(findKSmallest([3, 2, 1, 5, 6, 4], 2)); // [1, 2]
Kth Largest Element #
function findKthLargest(nums, k) {
const minHeap = new MinHeap();
for (const num of nums) {
minHeap.insert(num);
if (minHeap.size() > k) {
minHeap.extractMin();
}
}
return minHeap.peek();
}
console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 5
Top K Frequent Elements #
function topKFrequent(nums, k) {
const freq = new Map();
for (const num of nums) {
freq.set(num, (freq.get(num) || 0) + 1);
}
const minHeap = new PriorityQueue((a, b) => a[1] - b[1]);
for (const [num, count] of freq) {
minHeap.enqueue([num, count]);
if (minHeap.size() > k) {
minHeap.dequeue();
}
}
return minHeap.heap.map(x => x[0]);
}
console.log(topKFrequent([1, 1, 1, 2, 2, 3], 2)); // [1, 2]
Merge K Sorted Lists #
class ListNode {
constructor(val) {
this.val = val;
this.next = null;
}
}
function mergeKLists(lists) {
const pq = new PriorityQueue((a, b) => a.val - b.val);
// Add first node from each list
for (const list of lists) {
if (list) {
pq.enqueue(list);
}
}
const dummy = new ListNode(0);
let current = dummy;
while (!pq.isEmpty()) {
const node = pq.dequeue();
current.next = node;
current = current.next;
if (node.next) {
pq.enqueue(node.next);
}
}
return dummy.next;
}Find Median from Data Stream #
class MedianFinder {
constructor() {
this.maxHeap = new MaxHeap(); // Lower half
this.minHeap = new MinHeap(); // Upper half
}
addNum(num) {
// Add to max heap first
this.maxHeap.insert(num);
// Balance: max of lower half ≤ min of upper half
this.minHeap.insert(this.maxHeap.extractMax());
// Balance sizes
if (this.minHeap.size() > this.maxHeap.heap.length) {
this.maxHeap.insert(this.minHeap.extractMin());
}
}
findMedian() {
if (this.maxHeap.heap.length > this.minHeap.size()) {
return this.maxHeap.heap[0];
}
return (this.maxHeap.heap[0] + this.minHeap.heap[0]) / 2;
}
}
const mf = new MedianFinder();
mf.addNum(1);
mf.addNum(2);
console.log(mf.findMedian()); // 1.5
mf.addNum(3);
console.log(mf.findMedian()); // 2
Kth Smallest Element in Matrix #
function kthSmallest(matrix, k) {
const n = matrix.length;
const pq = new PriorityQueue((a, b) => a.val - b.val);
// Add first column
for (let i = 0; i < Math.min(n, k); i++) {
pq.enqueue({ val: matrix[i][0], row: i, col: 0 });
}
let result;
for (let i = 0; i < k; i++) {
const { val, row, col } = pq.dequeue();
result = val;
if (col + 1 < n) {
pq.enqueue({ val: matrix[row][col + 1], row, col: col + 1 });
}
}
return result;
}
const matrix = [
[1, 5, 9],
[10, 11, 13],
[12, 13, 15]
];
console.log(kthSmallest(matrix, 8)); // 13
Meeting Rooms II #
function minMeetingRooms(intervals) {
if (intervals.length === 0) return 0;
intervals.sort((a, b) => a[0] - b[0]);
const minHeap = new MinHeap();
minHeap.insert(intervals[0][1]);
for (let i = 1; i < intervals.length; i++) {
if (intervals[i][0] >= minHeap.peek()) {
minHeap.extractMin();
}
minHeap.insert(intervals[i][1]);
}
return minHeap.size();
}
console.log(minMeetingRooms([[0, 30], [5, 10], [15, 20]])); // 2
Task Scheduler #
function leastInterval(tasks, n) {
const freq = new Map();
for (const task of tasks) {
freq.set(task, (freq.get(task) || 0) + 1);
}
const maxHeap = new MaxHeap();
for (const count of freq.values()) {
maxHeap.insert(count);
}
let time = 0;
while (maxHeap.heap.length > 0) {
const temp = [];
for (let i = 0; i <= n; i++) {
if (maxHeap.heap.length > 0) {
temp.push(maxHeap.extractMax());
}
}
for (const count of temp) {
if (count - 1 > 0) {
maxHeap.insert(count - 1);
}
}
time += maxHeap.heap.length > 0 ? n + 1 : temp.length;
}
return time;
}
console.log(leastInterval(['A', 'A', 'A', 'B', 'B', 'B'], 2)); // 8
Reorganize String #
function reorganizeString(s) {
const freq = new Map();
for (const char of s) {
freq.set(char, (freq.get(char) || 0) + 1);
}
const maxHeap = new PriorityQueue((a, b) => b.count - a.count);
for (const [char, count] of freq) {
maxHeap.enqueue({ char, count });
}
const result = [];
let prev = null;
while (!maxHeap.isEmpty() || prev) {
if (prev && maxHeap.isEmpty()) {
return ''; // Impossible
}
const current = maxHeap.dequeue();
result.push(current.char);
current.count--;
if (prev) {
maxHeap.enqueue(prev);
prev = null;
}
if (current.count > 0) {
prev = current;
}
}
return result.join('');
}
console.log(reorganizeString('aab')); // 'aba'
console.log(reorganizeString('aaab')); // ''
Time Complexity #
| Operation | Time Complexity |
|---|---|
| Insert | O(log n) |
| Extract Min/Max | O(log n) |
| Peek | O(1) |
| Build Heap | O(n) |
| Heap Sort | O(n log n) |
| Remove | O(log n) |
Space Complexity #
- Heap: O(n)
- Priority Queue: O(n)
Best Practices #
- Use min/max heap appropriately - K largest → min heap
- Heapify for large datasets - O(n) vs O(n log n)
- Priority queue for scheduling - Task scheduling
- Two heaps for median - Split data
- Consider alternatives - Sometimes sorting is simpler
- Custom comparators - For complex objects
- Balance heap sizes - For median finding
Heaps and priority queues are essential for solving ordering and scheduling problems efficiently. Master these data structures for interview success.