Progress
🌍
Why does this topic matter?
Heaps appear in 15–20% of FAANG interviews. Three problem families dominate: Top-K (Kth largest, K closest points), Two Heaps (running median), and K-way Merge (merge K sorted lists). Master these three and you have a pattern for roughly 80% of all heap problems. Java's PriorityQueue is your primary tool — understand what it does internally so you never misuse it.
🔗
How it connects: A heap IS a complete binary tree (Lecture 15) stored in an array (Lecture 3). The parent-child formulas below come directly from BFS level-order indexing. After this lecture, heaps underpin Dijkstra's algorithm (Lecture 17 Graphs) and the median-of-medians approach in Sorting.

🏠 Heap Anatomy — The Array Representation

A complete binary tree can be stored perfectly in an array with no pointers needed. The structural rules are fixed by index arithmetic:

Array Representation — Index Formulas
 Index:   0    1    2    3    4    5    6
 Array: [ 1,   3,   5,   7,   9,   8,   6 ]   ← min-heap

 Visualised as a tree:
            1          ← index 0  (root)
          /   \
         3     5       ← index 1, 2
        / \   / \
       7   9 8   6     ← index 3, 4, 5, 6

 Parent of i  → (i - 1) / 2
 Left child   → 2*i + 1
 Right child  → 2*i + 2

 Heap Property (min-heap): parent ≤ both children at EVERY node
 Max-heap: flip to parent ≥ both children
📌
Key complexity facts:
Insert (offer): O(log n) — sift UP  |  Extract-min (poll): O(log n) — sift DOWN  |  Peek-min: O(1) — always at index 0  |  Build heap from array: O(n) — not O(n log n)!

Sift-Up & Sift-Down

☕ Java · Min-Heap from Scratch (sift-up + sift-down)
int[] heap; int size;

// INSERT: add at end, bubble up until heap property restored
void offer(int val) {
    heap[size++] = val;
    siftUp(size - 1);
}
void siftUp(int i) {
    while (i > 0) {
        int p = (i - 1) / 2;
        if (heap[p] <= heap[i]) break;  // already valid
        swap(i, p);
        i = p;
    }
}

// EXTRACT-MIN: swap root with last, shrink, sift root down
int poll() {
    int min = heap[0];
    heap[0] = heap[--size];
    siftDown(0);
    return min;
}
void siftDown(int i) {
    while (2*i+1 < size) {
        int left = 2*i+1, right = 2*i+2;
        int smallest = (right < size && heap[right] < heap[left]) ? right : left;
        if (heap[i] <= heap[smallest]) break;
        swap(i, smallest);
        i = smallest;
    }
}

// BUILD HEAP in O(n): sift-down from last internal node upward
void buildHeap(int[] arr) {
    heap = arr; size = arr.length;
    for (int i = size/2 - 1; i >= 0; i--) siftDown(i);
}
// Why O(n)? Leaf nodes need 0 work. Bottom half are all leaves.
// Formal proof: sum of sift-down heights = O(n) (geometric series)
📊
Dry Run — Insert 2 into min-heap [1,3,5,7,9]:
Before: heap = [1, 3, 5, 7, 9] 1 / \ 3 5 / \ 7 9 Insert 2 at index 5: heap = [1, 3, 5, 7, 9, 2] i=5, parent=(5-1)/2=2, heap[2]=5 > heap[5]=2 → swap heap = [1, 3, 2, 7, 9, 5] i=2, parent=(2-1)/2=0, heap[0]=1 ≤ heap[2]=2 → STOP ✓ After: heap = [1, 3, 2, 7, 9, 5] 1 / \ 3 2 ← min-heap property restored / \ / 7 9 5
💡
Java PriorityQueue cheatsheet:
PriorityQueue<Integer> minH = new PriorityQueue<>();    (default: min-heap)
PriorityQueue<Integer> maxH = new PriorityQueue<>(Collections.reverseOrder());
PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[0]-b[0]);  — sort by first element

Key methods: offer(x) O(log n)  |  poll() O(log n)  |  peek() O(1)  |  size() O(1)

🎯 The 5 Core Patterns

# Pattern Signal Phrase Tool Canonical Problem
1 Min/Max Heap Ops Kth largest, running max/min Single heap Kth Largest in Array
2 Top K Elements "top K", "K most frequent", "K closest" Size-K min-heap Top K Frequent, K Closest Points
3 Two Heaps (Median) "median", "sliding window median" maxH + minH balanced Median from Data Stream
4 K-way Merge "merge K sorted", "smallest from K" Min-heap of size K Merge K Sorted Lists
5 Greedy Heap "schedule", "reorganize", "cooldown" Max-heap + queue Task Scheduler, Reorganize String

📈 Pattern 1 — Single Heap (Kth Largest)

The simplest heap pattern. Use a size-K min-heap to track the K largest elements seen so far. The root (min of heap) is always the Kth largest. If a new element is larger than the root, pop root and push new element.

Pseudocode — Kth Largest

Maintain a min-heap of size K:
  for each num in nums:
    offer num to heap
    if heap.size() > K → poll (discard smallest)
  return heap.peek() — the Kth largest

Why size-K min-heap? The K largest elements survive. Their minimum (heap root) is exactly the Kth largest.

☕ Java · Kth Largest Element in Array (LC 215)
int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> minH = new PriorityQueue<>();  // min-heap
    for (int n : nums) {
        minH.offer(n);
        if (minH.size() > k) minH.poll();  // evict smallest; top K survive
    }
    return minH.peek();  // min of top-K = Kth largest
}
// Time O(n log k)  Space O(k)
// Why NOT sort? Sort is O(n log n). If k << n, heap is much faster.
📊
Dry Run — findKthLargest([3,2,1,5,6,4], k=2):
offer 3: heap=[3] offer 2: heap=[2,3] offer 1: heap=[1,2,3] size=3 > k=2 → poll 1 heap=[2,3] offer 5: heap=[2,3,5] size=3 > k=2 → poll 2 heap=[3,5] offer 6: heap=[3,5,6] size=3 > k=2 → poll 3 heap=[5,6] offer 4: heap=[4,5,6] size=3 > k=2 → poll 4 heap=[5,6] peek = 5 ✓ (5 is the 2nd largest in [3,2,1,5,6,4])

🏆 Pattern 2 — Top K Elements

A generalisation of Pattern 1. Any problem asking for the K most/least something follows this shape. Key insight: use a min-heap of size K for top-K largest (counterintuitive — min-heap, not max-heap).

Pseudocode — Top K Frequent

1. Count frequencies with a HashMap
2. Push (freq, num) into a min-heap of size K
3. If heap size > K, poll (removes least frequent)
4. Remaining K entries are the top-K frequent

Why min-heap? We want to EVICT the least frequent (smallest freq) to keep top-K. The min is always at the root, so we can discard it in O(log k).

☕ Java · Top K Frequent Elements (LC 347) + K Closest Points (LC 973)
// Top K Frequent Elements:
int[] topKFrequent(int[] nums, int k) {
    Map<Integer,Integer> freq = new HashMap<>();
    for (int n : nums) freq.merge(n, 1, Integer::sum);
    // min-heap: sort by frequency ascending → easily evict least frequent
    PriorityQueue<int[]> minH = new PriorityQueue<>((a,b) -> a[0]-b[0]);
    for (var e : freq.entrySet()) {
        minH.offer(new int[]{e.getValue(), e.getKey()});
        if (minH.size() > k) minH.poll();
    }
    int[] res = new int[k];
    for (int i = k-1; i >= 0; i--) res[i] = minH.poll()[1];
    return res;
}
// Time O(n log k)  Space O(n) for freq map

// K Closest Points to Origin — same pattern, different comparator:
int[][] kClosest(int[][] pts, int k) {
    // max-heap by distance — evict farthest, keep K closest
    PriorityQueue<int[]> maxH = new PriorityQueue<>(
        (a,b) -> (b[0]*b[0]+b[1]*b[1]) - (a[0]*a[0]+a[1]*a[1]));
    for (int[] p : pts) {
        maxH.offer(p);
        if (maxH.size() > k) maxH.poll();  // evict farthest
    }
    return maxH.toArray(new int[0][]);
}
// Note: use squared distance to avoid floating point sqrt

⚖ Pattern 3 — Two Heaps (Running Median)

Split the data stream into two halves: a max-heap for the lower half and a min-heap for the upper half. Keep their sizes balanced (differ by at most 1). The median is always at one of the two roots.

Pseudocode — Two Heaps Median

maxH = max-heap (lower half)  |  minH = min-heap (upper half)

addNum(n):
  offer n to maxH
  offer maxH.poll() to minH   (push max-of-lower up)
  if minH.size() > maxH.size() → offer minH.poll() to maxH

findMedian():
  if sizes equal → (maxH.peek() + minH.peek()) / 2.0
  else → maxH.peek()  (maxH always has the extra element)

☕ Java · Find Median from Data Stream (LC 295) — Hard
PriorityQueue<Integer> maxH = new PriorityQueue<>(Collections.reverseOrder()); // lower half
PriorityQueue<Integer> minH = new PriorityQueue<>();                           // upper half

void addNum(int n) {
    maxH.offer(n);
    minH.offer(maxH.poll());        // ensure max-of-lower ≤ min-of-upper
    if (minH.size() > maxH.size()) maxH.offer(minH.poll());  // rebalance
}

double findMedian() {
    if (maxH.size() == minH.size()) return (maxH.peek() + minH.peek()) / 2.0;
    return maxH.peek();  // maxH has the extra element
}
// addNum: O(log n)  findMedian: O(1)
// Invariant: maxH.peek() ≤ minH.peek() always (cross-push enforces this)
📊
Dry Run — addNum sequence [1, 2, 3]:
addNum(1): maxH.offer(1)=[1] minH.offer(maxH.poll()=1)=[1] maxH=[] minH.size()=1 > maxH.size()=0 → maxH.offer(minH.poll()=1) maxH=[1] minH=[] findMedian()=1.0 addNum(2): maxH.offer(2)=[1,2] minH.offer(maxH.poll()=2)=[2] maxH=[1] sizes equal(1,1) findMedian()=(1+2)/2=1.5 addNum(3): maxH.offer(3)=[1,3] minH.offer(maxH.poll()=3)=[2,3] maxH=[1] minH.size()=2 > maxH.size()=1 → maxH.offer(minH.poll()=2) maxH=[2,1] minH=[3] findMedian()=2.0

🔁 Pattern 4 — K-way Merge

Given K sorted lists (or arrays), use a min-heap to efficiently pull the globally smallest element each time. Always keep exactly one element from each active list in the heap.

Pseudocode — K-way Merge

1. Push the first element of each list into min-heap (with list index + position)
2. While heap not empty:
    Poll the smallest element → add to result
    If that list has more elements → push the next one

Key: heap always has at most K elements → poll is O(log K) not O(log n)

☕ Java · Merge K Sorted Lists (LC 23)
ListNode mergeKLists(ListNode[] lists) {
    // min-heap ordered by node value
    PriorityQueue<ListNode> pq = new PriorityQueue<>((a,b) -> a.val - b.val);
    for (ListNode head : lists) if (head != null) pq.offer(head);

    ListNode dummy = new ListNode(0), curr = dummy;
    while (!pq.isEmpty()) {
        curr.next = pq.poll();          // pull globally smallest
        curr = curr.next;
        if (curr.next != null) pq.offer(curr.next);  // push next from same list
    }
    return dummy.next;
}
// Time O(N log K) where N = total nodes, K = number of lists
// Space O(K) for the heap — only K nodes live in the heap at any time
📊
Dry Run — Merge K=3 lists [[1,4,5],[1,3,4],[2,6]]:
Initial heap (heads): [1(L1), 1(L2), 2(L3)] Step 1: poll 1(L1) → result=[1] push 4(L1) heap=[1(L2),2(L3),4(L1)] Step 2: poll 1(L2) → result=[1,1] push 3(L2) heap=[2,3,4] Step 3: poll 2(L3) → result=[1,1,2] push 6(L3) heap=[3,4,6] Step 4: poll 3(L2) → result=[1,1,2,3] push 4(L2) heap=[4,4,6] Step 5: poll 4(L1) → result=[...,4] push 5(L1) heap=[4,5,6] Step 6: poll 4(L2) → result=[...,4] L2 done heap=[5,6] Step 7: poll 5(L1) → result=[...,5] L1 done heap=[6] Step 8: poll 6(L3) → result=[...,6] done ✓ Result: 1→1→2→3→4→4→5→6

⚡ Pattern 5 — Greedy Heap (Task Scheduler)

Use a max-heap to always process the most frequent/highest-priority task next. When a cooldown constraint exists, pair the heap with a wait queue.

Pseudocode — Task Scheduler

maxH = max-heap of task frequencies
queue = [(freq_after_task, available_at_time)]

while maxH or queue not empty:
  time++
  if maxH not empty → pick most frequent, decrement, push to queue with (freq-1, time+n)
  if queue.front.available_at ≤ time → push back to maxH

☕ Java · Task Scheduler (LC 621) + Reorganize String (LC 767)
// Task Scheduler — greedy: always do the most-frequent available task
int leastInterval(char[] tasks, int n) {
    int[] cnt = new int[26];
    for (char c : tasks) cnt[c-'A']++;
    PriorityQueue<Integer> maxH = new PriorityQueue<>(Collections.reverseOrder());
    for (int c : cnt) if (c > 0) maxH.offer(c);
    Queue<int[]> waitQ = new LinkedList<>();   // [remaining_count, available_at]
    int time = 0;
    while (!maxH.isEmpty() || !waitQ.isEmpty()) {
        time++;
        if (!maxH.isEmpty()) {
            int rem = maxH.poll() - 1;
            if (rem > 0) waitQ.offer(new int[]{rem, time + n});
        }
        if (!waitQ.isEmpty() && waitQ.peek()[1] == time) maxH.offer(waitQ.poll()[0]);
    }
    return time;
}
// Time O(T * 26) where T = total tasks  Space O(26) = O(1)

// Reorganize String — same greedy idea:
// Always pick the most frequent char that != previous char
String reorganizeString(String s) {
    int[] cnt = new int[26];
    for (char c : s.toCharArray()) cnt[c-'a']++;
    PriorityQueue<int[]> maxH = new PriorityQueue<>((a,b) -> b[0]-a[0]);
    for (int i = 0; i < 26; i++) if (cnt[i]>0) maxH.offer(new int[]{cnt[i], i});
    StringBuilder sb = new StringBuilder();
    while (maxH.size() >= 2) {
        int[] a = maxH.poll(), b = maxH.poll();
        sb.append((char)('a'+a[1])); sb.append((char)('a'+b[1]));
        if (--a[0] > 0) maxH.offer(a);
        if (--b[0] > 0) maxH.offer(b);
    }
    if (!maxH.isEmpty()) {
        if (maxH.peek()[0] > 1) return "";  // impossible
        sb.append((char)('a'+maxH.poll()[1]));
    }
    return sb.toString();
}
// Time O(n log 26) = O(n)  Space O(n) for output

💪 In-Lecture Practice Problems

Problem 01 · LC 215
Kth Largest Element in an Array
Medium Pattern 1: Single Min-Heap Amazon · Google · Facebook
Problem 02 · LC 347
Top K Frequent Elements
Medium Pattern 2: Top K Amazon · Google · Facebook
Problem 03 · LC 295
Find Median from Data Stream
Hard Pattern 3: Two Heaps Amazon · Google · Microsoft
Problem 04 · LC 23
Merge K Sorted Lists
Hard Pattern 4: K-way Merge Amazon · Google · Microsoft
Problem 05 · LC 621
Task Scheduler
Medium Pattern 5: Greedy Heap Amazon · Facebook

📝 Assignment — Company Interview Problems

📋
Topic 16 Assignment — 20 Problems (5 Easy · 10 Medium · 5 Hard)
All 5 heap patterns covered. Every problem tagged with pattern, company, and difficulty.

📄 Open Assignment.md →

✅ Lecture Completion Checklist

Check each item before advancing to Lecture 17 (Graphs). Be honest — these are your interview readiness criteria.

I can explain the array representation of a heap (parent/child index formulas) without looking it up
I can implement sift-up and sift-down from scratch and explain why build-heap is O(n), not O(n log n)
I know when to use a min-heap vs max-heap for Top-K problems (counterintuitive: use min-heap for K largest)
I can implement the Two Heaps pattern (addNum + findMedian) from memory in under 2 minutes
I understand why K-way merge is O(N log K) and not O(N log N)
I can write a custom PriorityQueue comparator for arrays, objects, and lambda expressions
I have completed all 20 assignment problems and can identify the pattern within 30 seconds
You are ready for Topic 17: Graphs
Graphs generalise trees: a node can have more than 2 neighbours. BFS and DFS patterns you used on trees apply directly. The heap you built here powers Dijkstra's shortest path algorithm in Graphs.
← Topic 15: Trees & BST Topic 16 of 30 — Phase 2 Topic 17: Graphs →