DAY 22 / 30 · WEEK 4

Heaps & Priority Queues

Goal of the day

Concept

A binary heap is a complete binary tree — every level is completely filled except possibly the last, which fills left to right with no gaps — that also satisfies the heap property. For a min-heap (today's focus), the heap property says every parent is less than or equal to both of its children, so the root is always the smallest value in the whole structure. A max-heap is the mirror image: every parent is greater than or equal to both children, so the root is always the largest — same code throughout, just flip every comparison.

Because the tree is always complete (no gaps), you never actually need pointers to represent it. Instead, you store the values in a plain array, left to right, level by level, and use index arithmetic to find a node's parent and children: for a node at index i, its children live at index 2i + 1 and index 2i + 2, and its parent lives at index floor((i − 1) / 2). No Node class, no .left/.right references — just an array and three formulas. This is the array-based heap almost every interview question expects, and it's the reason the tree diagram below and the array boxes above it are two views of the exact same data.

Real-life analogy. Think of a hospital ER's triage queue. Patients aren't seen in the order they walked in — the most urgent case is always seen next, no matter how long everyone else has been waiting. That's a min-heap ordered by urgency, and it's the opposite of Day 18's queue: a FIFO queue guarantees first-come-first-served regardless of how important each item is, while a heap-backed priority queue guarantees most-urgent-first regardless of arrival order.

Diagram: array-to-tree index mapping

Array [3, 8, 5, 15, 12, 9, 20] — same data, two views i=0 i=1 i=2 i=3 i=4 i=5 i=6 3 8 5 15 12 9 20 same 7 values, laid out as a tree ↓ 3 8 5 15 12 9 20 i=0 i=1 i=2 i=3 i=4 i=5 i=6 left child of i → index 2i + 1 right child of i → index 2i + 2 parent of i → index floor((i - 1) / 2)

Diagram: bubble-up, step by step

Insert 3 into heap [10, 15, 12] — bubble up toward the root Before — 3 appended as index 3, a child of index 1 (value 15) 10 15 12 3 ① 15 is greater than 3 — swap index 3 and index 1 10 3 12 15 ② 10 is greater than 3 — swap index 1 and index 0, done 3 10 12 15 Final: [3, 10, 12, 15] — 3 only traveled as far up as it needed to.

Interactive visualization: insert + bubble up

current node (bubbling) parent being compared swap in progress heap property confirmed

Array view (flat storage)

Tree view (same data, same indices)

Press Play or Step to begin.

Interactive visualization: extract-min + bubble down

root about to be removed current node (bubbling) children being compared swap in progress heap property confirmed

Array view (flat storage)

Tree view (same data, same indices)

Press Play or Step to begin.

The code: a min-heap on a plain array

class MinHeap {
  constructor() {
    this.data = []; // flat array — this IS the whole heap, no nodes, no pointers
  }

  size() { return this.data.length; }
  peek() { return this.data[0]; }               // O(1) — the min always sits at the root

  // index math takes the place of .parent/.left/.right pointers
  parent(i) { return Math.floor((i - 1) / 2); }
  left(i)   { return 2 * i + 1; }
  right(i)  { return 2 * i + 2; }

  insert(val) {
    this.data.push(val);                   // 1. add to the end — the only O(1) place to insert
    this.bubbleUp(this.data.length - 1);   // 2. restore the heap property upward
  }

  bubbleUp(i) {
    while (i > 0) {
      const p = this.parent(i);
      if (this.data[p] <= this.data[i]) break; // parent already smaller or equal — done
      [this.data[p], this.data[i]] = [this.data[i], this.data[p]]; // swap up
      i = p;                                      // keep bubbling from the new position
    }
  }

  extractMin() {
    if (this.data.length === 0) return undefined;
    const min = this.data[0];        // 1. the root is always the minimum
    const last = this.data.pop();    // 2. remove the last element
    if (this.data.length > 0) {
      this.data[0] = last;           // 3. move it to the root to keep the array compact
      this.bubbleDown(0);            // 4. restore the heap property downward
    }
    return min;
  }

  bubbleDown(i) {
    const n = this.data.length;
    while (true) {
      const l = this.left(i), r = this.right(i);
      let smallest = i;
      if (l < n && this.data[l] < this.data[smallest]) smallest = l;
      if (r < n && this.data[r] < this.data[smallest]) smallest = r; // check BOTH children
      if (smallest === i) break;      // neither child is smaller — done
      [this.data[i], this.data[smallest]] = [this.data[smallest], this.data[i]];
      i = smallest;                   // keep bubbling from the new position
    }
  }
}

Line by line:

Dry run: extractMin() bubble-down on [3, 8, 5, 15, 12, 9, 20]

min = arr[0] = 3 (saved as the return value). Pop the last element, 20, and move it into the root — the array entering the loop below is [20, 8, 5, 15, 12, 9].

iarr[i]left childright childAction
020idx 1 = 8idx 2 = 55 is the smaller of the two children — swap index 0 and index 2
220idx 5 = 9none (idx 6 doesn't exist)9 is the only, and therefore smaller, child — swap index 2 and index 5
520nonenoneno children left to compare — heap property holds, stop

Two swaps later the heap is [5, 8, 9, 15, 12, 20] and extractMin() returns 3 — matching the interactive visualization above, which uses this exact same starting array.

Complexity

OperationTimeSpaceWhy
insert (push + bubble up)O(log n)O(1)The new value travels at most the height of the tree on its way up (log n levels), and each level costs one comparison plus at most one swap.
extractMin (bubble down)O(log n)O(1)Same bound in the other direction — the replacement root sinks at most the height of the tree before both of its children are no smaller than it.
peekO(1)O(1)The minimum always sits at index 0 — no traversal needed.
build heap from an unsorted arrayO(n)O(1)This one is surprising — see the note below the table.

Why build-heap is O(n), not O(n log n): the standard bottom-up algorithm runs bubbleDown starting from the last non-leaf node up through the root — it does not insert elements one at a time. Most nodes in a complete tree live near the bottom, where a bubbleDown call has almost no distance left to travel (a leaf does zero work, the row just above leaves does at most one swap, and so on). Summing that shrinking amount of work across every level of the tree converges to O(n) total. The naive "n inserts, each O(log n)" estimate of O(n log n) overcounts badly, because it assumes every node needs a full root-to-leaf (or leaf-to-root) walk, when in a heap the vast majority of nodes are already close to where they need to be.

Interview corner

How to talk through it out loud: say the invariant before touching the array — "index i's children live at 2i + 1 and 2i + 2, its parent at floor((i − 1) / 2), and every parent has to be less than or equal to both children." For insert: "push to the end, then bubble up — compare against the parent, swap if it's bigger, stop the moment it isn't." For extractMin: "save the root as the return value, move the last element into the root, shrink the array by one, then bubble down — compare against both children, swap with whichever is smaller, never just the left one." Interviewers are listening for two specific things: whether you get the index math right without hesitating, and whether you remember to check both children on the way down — those are the two places this pattern silently breaks in a live interview.

Practice problems

1. Kth Largest Element in a Stream (Easy — LeetCode: Kth Largest Element in a Stream)

Design a class that finds the kth largest element in a stream of numbers as new values are added one at a time — this is a running, online query, not a one-shot computation.

2. Last Stone Weight (Easy — LeetCode: Last Stone Weight)

Given an array of stone weights, repeatedly smash the two heaviest stones together: if they're equal, both are destroyed; otherwise the lighter one is destroyed and the heavier becomes the difference between the two. Return the weight of the last stone left, or 0 if none remain.

3. Top K Frequent Elements (Medium — LeetCode: Top K Frequent Elements)

Given an integer array, return the k most frequent elements, in any order.

Quiz

1. For a 0-indexed array-based heap, what are the child indices of a node at index i?

2. When bubbling down, why must you compare against both children instead of just the left one?

3. What is the time complexity of peek() — reading the minimum without removing it?

4. Building a heap from an existing n-element array (heapify) runs in what time complexity?

5. In the array [10, 15, 20, 17, 25, 30, 40] (0-indexed), what is the parent index of index 5?