DSAHeaps

Heaps

A heap is a complete binary tree that satisfies the heap property: in a max-heap every parent is ≥ its children; in a min-heap every parent is ≤ its children. Heaps power priority queues, heap sort, and classic interview problems like top-K and the median of a data stream.

Heap Property (Min vs Max)

Text
MAX-HEAP                        MIN-HEAP
      100                               1
      / \                             / \
    19   36                          3   6
   /  \  / \                        / \ / \
  17  3 25  1                      5  9 8  7

Parent ≥ children                  Parent ≤ children
Root = largest element             Root = smallest element

Heap type

Root contains

Common use

Max-heap

Maximum element

Priority queue (highest priority first)

Min-heap

Minimum element

Dijkstra's, event simulation, median tracking

Note
A heap is NOT a fully sorted structure. It only guarantees the parent-child relationship, not sibling order. Searching for an arbitrary value is O(n).
Complete Binary Tree Stored as Array

Because a heap is a complete binary tree (every level is fully filled except possibly the last, which fills left-to-right), it maps perfectly onto an array with zero wasted space and no pointers needed.

Text
Tree:                Array (1-indexed):
        100              index: 1   2   3   4   5   6   7
        / \              value:[100, 19, 36, 17,  3, 25,  1]
      19   36
     /  \  / \
   17   3 25   1

Index formulas (1-indexed):
  parent(i)      = floor(i / 2)
  left-child(i)  = 2 * i
  right-child(i) = 2 * i + 1

Index formulas (0-indexed — more common in code):
  parent(i)      = floor((i - 1) / 2)
  left-child(i)  = 2 * i + 1
  right-child(i) = 2 * i + 2
Tip
0-indexed arrays are standard in JavaScript. All code below uses 0-indexed formulas.
Heapify Up (Bubble Up)

Used after inserting a new element at the end of the array. The element "bubbles up" until the heap property is restored.

Text
Insert 50 into max-heap [100, 19, 36, 17, 3, 25, 1]:

Step 1 — append at end (index 7):
  [100, 19, 36, 17, 3, 25, 1, 50]
              50 is at index 7, parent = floor((7-1)/2) = 3 → value 17

Step 2 — 50 > 17, swap:
  [100, 19, 36, 50, 3, 25, 1, 17]
              50 is at index 3, parent = floor((3-1)/2) = 1 → value 19

Step 3 — 50 > 19, swap:
  [100, 50, 36, 19, 3, 25, 1, 17]
              50 is at index 1, parent = 0 → value 100

Step 4 — 50 < 100, stop. Heap property restored.
Heapify Down (Sift Down)

Used after removing the root (the max/min). Swap the root with the last element, remove the last element, then sift the new root down.

Text
Extract max from [100, 50, 36, 19, 3, 25, 1]:

Step 1 — swap root with last element:
  [1, 50, 36, 19, 3, 25, 100]   remove 100

Step 2 — sift down 1 at index 0:
  children: left=50 (idx 1), right=36 (idx 2), max child = 50
  1 < 50, swap → [50, 1, 36, 19, 3, 25]

Step 3 — sift down 1 at index 1:
  children: left=19 (idx 3), right=3 (idx 4), max child = 19
  1 < 19, swap → [50, 19, 36, 1, 3, 25]

Step 4 — 1 at index 3 has no children (left would be idx 7, out of bounds). Stop.

Result: [50, 19, 36, 1, 3, 25]   extracted max = 100
MaxHeap — Full JavaScript Implementation

JS
class MaxHeap {
  constructor() { this.heap = []; }

  size()    { return this.heap.length; }
  peek()    { return this.heap[0]; }        // max element, O(1)
  isEmpty() { return this.size() === 0; }

  // ── index helpers ──────────────────────────────────────
  parent(i) { return Math.floor((i - 1) / 2); }
  left(i)   { return 2 * i + 1; }
  right(i)  { return 2 * i + 2; }

  swap(i, j) {
    [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
  }

  // ── insert  O(log n) ──────────────────────────────────
  insert(val) {
    this.heap.push(val);
    this.heapifyUp(this.size() - 1);
  }

  heapifyUp(i) {
    while (i > 0) {
      const p = this.parent(i);
      if (this.heap[p] < this.heap[i]) {
        this.swap(p, i);
        i = p;
      } else break;
    }
  }

  // ── extract max  O(log n) ────────────────────────────
  extractMax() {
    if (this.isEmpty()) return null;
    if (this.size() === 1) return this.heap.pop();

    const max = this.heap[0];
    this.heap[0] = this.heap.pop();     // move last to root
    this.heapifyDown(0);
    return max;
  }

  heapifyDown(i) {
    const n = this.size();
    while (true) {
      let largest = i;
      const l = this.left(i), r = this.right(i);
      if (l < n && this.heap[l] > this.heap[largest]) largest = l;
      if (r < n && this.heap[r] > this.heap[largest]) largest = r;
      if (largest === i) break;
      this.swap(i, largest);
      i = largest;
    }
  }
}

// ── MinHeap: just flip the comparison signs ────────────
class MinHeap extends MaxHeap {
  heapifyUp(i) {
    while (i > 0) {
      const p = this.parent(i);
      if (this.heap[p] > this.heap[i]) { this.swap(p, i); i = p; }
      else break;
    }
  }

  heapifyDown(i) {
    const n = this.size();
    while (true) {
      let smallest = i;
      const l = this.left(i), r = this.right(i);
      if (l < n && this.heap[l] < this.heap[smallest]) smallest = l;
      if (r < n && this.heap[r] < this.heap[smallest]) smallest = r;
      if (smallest === i) break;
      this.swap(i, smallest);
      i = smallest;
    }
  }

  extractMin() { return this.extractMax(); }  // same logic, different comparator
}
Build Heap in O(n)

Inserting n elements one-by-one costs O(n log n). But we can build a heap from an unsorted array in O(n) by calling heapifyDown on every non-leaf node, starting from the last one and working upward.

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

Nodes at height h can sink at most h levels.
  - n/2 nodes at height 0 (leaves):   0 work each
  - n/4 nodes at height 1:            1 swap each  →  n/4 · 1
  - n/8 nodes at height 2:            2 swaps each →  n/8 · 2
  - ...
  - 1 node at height log n:           log n swaps  →  1 · log n

Sum = n · Σ (h / 2^h) = n · 2 = O(n)   (geometric series)

JS
function buildMaxHeap(arr) {
  const n = arr.length;
  // Last non-leaf is at index floor(n/2) - 1
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
    heapifyDown(arr, n, i);
  }
  return arr;
}

function heapifyDown(arr, n, i) {
  let largest = i;
  const l = 2 * i + 1, r = 2 * i + 2;
  if (l < n && arr[l] > arr[largest]) largest = l;
  if (r < n && arr[r] > arr[largest]) largest = r;
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    heapifyDown(arr, n, largest);
  }
}

// Demo
const arr = [3, 1, 6, 5, 2, 4];
buildMaxHeap(arr);
// arr → [6, 5, 4, 1, 2, 3]
Heap Sort

Heap sort sorts an array in-place in O(n log n) with O(1) space. Build a max-heap, then repeatedly extract the max (swap root with last, shrink heap, heapify down).

JS
function heapSort(arr) {
  const n = arr.length;

  // Phase 1: build max-heap  O(n)
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--)
    heapifyDown(arr, n, i);

  // Phase 2: extract elements  O(n log n)
  for (let end = n - 1; end > 0; end--) {
    [arr[0], arr[end]] = [arr[end], arr[0]];  // move current max to sorted end
    heapifyDown(arr, end, 0);                  // re-heapify the shrunk heap
  }
  return arr;
}

heapSort([5, 3, 8, 4, 2]);
// → [2, 3, 4, 5, 8]

Property

Heap Sort

Merge Sort

Quick Sort

Time (worst)

O(n log n)

O(n log n)

O(n²)

Time (average)

O(n log n)

O(n log n)

O(n log n)

Space

O(1)

O(n)

O(log n)

Stable

No

Yes

No (typical)

Cache friendly

Poor

Good

Good

Note
Heap sort has poor cache performance because array accesses jump around (parent-child indices are far apart). In practice, introsort (used by most standard libraries) falls back to heap sort only when recursion depth is too deep — it uses quick sort for the common case.
Top-K Problems

Finding the K largest (or smallest) elements is a classic heap problem. The key insight is to maintain a min-heap of size K while scanning the array.

JS
// Find K largest elements  — Time: O(n log k)  Space: O(k)
function topK(nums, k) {
  const minH = new MinHeap();

  for (const num of nums) {
    minH.insert(num);
    if (minH.size() > k) minH.extractMin();  // evict the smallest
  }

  // minH now contains the k largest elements
  return minH.heap;
}

topK([3, 2, 1, 5, 6, 4], 2);  // → [5, 6]  (heap order, not sorted)

// Why a min-heap?
// The root is the smallest of the top-k.
// When a new number arrives that is larger, it beats out the current minimum.
// This is more efficient than a max-heap of size n (O(n) space).

Kth largest element (LeetCode 215):

JS
function findKthLargest(nums, k) {
  const minH = new MinHeap();
  for (const n of nums) {
    minH.insert(n);
    if (minH.size() > k) minH.extractMin();
  }
  return minH.peek();   // root = kth largest
}
// Time: O(n log k)   Space: O(k)

// Alternative: O(n) average with quickselect, but heap is simpler to code
Median of a Data Stream

One of the most elegant heap problems. Maintain two heaps that together hold all numbers seen so far:

  • Max-heap (lo): holds the smaller half of the numbers. Root = largest of the small half.

  • Min-heap (hi): holds the larger half. Root = smallest of the large half.

  • Invariant: lo.size() == hi.size() OR lo.size() == hi.size() + 1

  • Median: if sizes are equal → average of both roots; else → root of lo.

Text
Stream: 5, 15, 1, 3

After 5:   lo = [5]          hi = []          median = 5
After 15:  lo = [5]          hi = [15]         median = (5+15)/2 = 10
After 1:   lo = [5, 1]       hi = [15]         median = 5
After 3:   lo = [5, 3, 1]    hi = [15]         median = 5

lo stores lower half, hi stores upper half.
lo.peek() ≤ hi.peek() always.

JS
class MedianFinder {
  constructor() {
    this.lo = new MaxHeap();   // lower half
    this.hi = new MinHeap();   // upper half
  }

  addNum(num) {
    // 1. Push into lo (always push to lo first)
    this.lo.insert(num);

    // 2. Balance: lo's max must be ≤ hi's min
    if (this.hi.size() > 0 && this.lo.peek() > this.hi.peek()) {
      this.hi.insert(this.lo.extractMax());
    }

    // 3. Keep sizes balanced: lo may have at most 1 extra element
    if (this.lo.size() > this.hi.size() + 1) {
      this.hi.insert(this.lo.extractMax());
    } else if (this.hi.size() > this.lo.size()) {
      this.lo.insert(this.hi.extractMin());
    }
  }

  findMedian() {
    if (this.lo.size() === this.hi.size()) {
      return (this.lo.peek() + this.hi.peek()) / 2;
    }
    return this.lo.peek();    // lo has the extra element
  }
}

// Demo
const mf = new MedianFinder();
mf.addNum(1);  mf.findMedian();  // 1
mf.addNum(2);  mf.findMedian();  // 1.5
mf.addNum(3);  mf.findMedian();  // 2
Tip
The two-heap trick generalises: replace "median" with any percentile. For the p-th percentile maintain lo.size() / (lo.size() + hi.size()) ≈ p/100.
Complexity Summary

Operation

Time

Space

Notes

Insert

O(log n)

O(1)

Heapify up from leaf

Extract min/max

O(log n)

O(1)

Heapify down from root

Peek min/max

O(1)

O(1)

Root of array

Build heap

O(n)

O(1)

Floyd algorithm

Heap sort

O(n log n)

O(1)

In-place

Top-K

O(n log k)

O(k)

Min-heap of size k

Median stream

O(log n) per insert

O(n)

Two heaps

More Interview Problems

K closest points to origin (LeetCode 973):

JS
function kClosest(points, k) {
  // Max-heap by distance; evict farthest when size > k
  const dist = ([x, y]) => x * x + y * y;
  const maxH = [];   // [distance, point] pairs

  const siftDown = (i) => {
    const n = maxH.length;
    while (true) {
      let largest = i;
      const l = 2*i+1, r = 2*i+2;
      if (l < n && maxH[l][0] > maxH[largest][0]) largest = l;
      if (r < n && maxH[r][0] > maxH[largest][0]) largest = r;
      if (largest === i) break;
      [maxH[i], maxH[largest]] = [maxH[largest], maxH[i]];
      i = largest;
    }
  };
  const siftUp = (i) => {
    while (i > 0) {
      const p = Math.floor((i-1)/2);
      if (maxH[p][0] < maxH[i][0]) { [maxH[p], maxH[i]] = [maxH[i], maxH[p]]; i = p; }
      else break;
    }
  };

  for (const pt of points) {
    maxH.push([dist(pt), pt]);
    siftUp(maxH.length - 1);
    if (maxH.length > k) {
      maxH[0] = maxH.pop();
      siftDown(0);
    }
  }

  return maxH.map(([, pt]) => pt);
}
// Time: O(n log k)   Space: O(k)

Merge K sorted lists (LeetCode 23) — use a min-heap of (value, listNode) pairs:

JS
function mergeKLists(lists) {
  // Min-heap stores [val, listIndex, node]
  const heap = [];

  const push = (item) => {
    heap.push(item);
    let i = heap.length - 1;
    while (i > 0) {
      const p = Math.floor((i-1)/2);
      if (heap[p][0] > heap[i][0]) { [heap[p], heap[i]] = [heap[i], heap[p]]; i = p; }
      else break;
    }
  };

  const pop = () => {
    const min = heap[0];
    heap[0] = heap.pop();
    let i = 0;
    while (true) {
      let smallest = i;
      const l = 2*i+1, r = 2*i+2;
      if (l < heap.length && heap[l][0] < heap[smallest][0]) smallest = l;
      if (r < heap.length && heap[r][0] < heap[smallest][0]) smallest = r;
      if (smallest === i) break;
      [heap[i], heap[smallest]] = [heap[smallest], heap[i]];
      i = smallest;
    }
    return min;
  };

  // Seed heap with the head of each list
  for (let i = 0; i < lists.length; i++)
    if (lists[i]) push([lists[i].val, i, lists[i]]);

  const dummy = { next: null };
  let cur = dummy;

  while (heap.length) {
    const [, idx, node] = pop();
    cur.next = node;
    cur = cur.next;
    if (node.next) push([node.next.val, idx, node.next]);
  }
  return dummy.next;
}
// Time: O(n log k) where n = total nodes, k = number of lists
Warning
JavaScript does not have a built-in priority queue. In interviews you may implement a simple heap inline as shown above, or use a sorted array as a stand-in for small inputs. In production code, use a library like @datastructures-js/priority-queue.