Heaps & Intervals

Two patterns that show up constantly: a heap for top-K / streaming / merging, and sort-then-sweep for interval problems.

heappriority-queueintervalssorting

Why We Need a Heap (Priority Queue)

In many problems, you need to repeatedly access the smallest or largest element in a dynamically changing set of numbers (e.g., finding the K closest points in a stream of incoming data). If you sort the set every time, it costs $O(n \log n)$ per insert. A Heap solves this by keeping the elements in a special binary tree structure that lets us insert and remove the extreme element in $O(\log n)$ time, and peek at it in $O(1)$ time.

When to Use a Heap


Heap Complexity

OperationTime ComplexityWhy
Peek Min/MaxO(1)The extreme value is always at the root
Push / InsertO(log n)Sifts up along the height of the tree
Pop / ExtractO(log n)Swaps root with last element, then sifts down
Heapify (Build)O(n)Mathematical optimization building bottom-up

Heap = priority queue

A binary heap gives O(log n) push/pop and O(1) peek of the min (or max). Reach for it whenever you repeatedly need the current extreme of a changing set.

Top-K with a size-K heap

To find the K largest elements in a dataset, maintain a min-heap of size K: push each element, and if the heap size exceeds K, pop the smallest. What remains in the heap at the end are the K largest elements.

# Python
import heapq

def k_largest(nums, k):
    h = []
    for x in nums:
        heapq.heappush(h, x)
        if len(h) > k:
            heapq.heappop(h)     # evict the smallest
    return h                      # the k largest (unordered)
# Time O(n log k), Space O(k)
Java
// Java
import java.util.ArrayList;
import java.util.List;
import java.util.PriorityQueue;

public List<Integer> kLargest(int[] nums, int k) {
    PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // default is min-heap
    for (int x : nums) {
        minHeap.add(x);
        if (minHeap.size() > k) {
            minHeap.poll();      // evict the smallest
        }
    }
    return new ArrayList<>(minHeap);
}
C++
// C++
#include <vector>
#include <queue>

std::vector<int> kLargest(const std::vector<int>& nums, int k) {
    // std::greater<int> makes it a min-heap
    std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
    for (int x : nums) {
        minHeap.push(x);
        if (minHeap.size() > k) {
            minHeap.pop();       // evict the smallest
        }
    }
    std::vector<int> result;
    while (!minHeap.empty()) {
        result.push_back(minHeap.top());
        minHeap.pop();
    }
    return result;
}

Other heap classics

  • Merge K sorted lists — push the head of each list; pop the min, push its successor. O(N log K).
  • Streaming median — a max-heap of the lower half + a min-heap of the upper half, kept balanced; median is the top(s). O(log n) per insert.
Top-K: heap vs sort vs quickselect

Sorting is O(n log n). A size-K heap is O(n log k) — better when k is small. Quickselect finds the kth element in O(n) average (O(n²) worst) and partitions around it. Mention all three and pick by constraints; the heap also handles streaming input, which sort/quickselect can't.

Watch it run

The same heap shown both ways at once: the flat array that's actually in memory, and the complete binary tree it encodes. Watch values sift up on push and sift down after extract-min.

Min-heap — push all, then extract-mintime O(log n) per opspace O(1) aux
(empty)

1/22Min-heap stored in an array: parent of i is ⌊(i−1)/2⌋, children are 2i+1 and 2i+2. Parent ≤ children, always.

Intervals = sort first, then sweep

Watch the sweep in action — after sorting by start, every new interval either overlaps the block being built (stretch it) or starts a fresh one (gap). One pass, no backtracking:

Merge overlapping intervalstime O(n log n)space O(n)
02468101214161820132681015181720merged

1/8Merge every overlapping pair into one block (think: calendar busy-bars). Unsorted, overlaps are scattered everywhere…

Almost every interval problem starts by sorting by start (or end), then making one pass.

Python
# Python — Merge overlapping intervals
def merge(intervals):
    intervals.sort(key=lambda iv: iv[0])
    out = [intervals[0]]
    for s, e in intervals[1:]:
        if s <= out[-1][1]:           # overlaps the last merged interval
            out[-1][1] = max(out[-1][1], e)
        else:
            out.append([s, e])
    return out
# Time O(n log n) sorting, Space O(n)
Java
// Java — Merge overlapping intervals
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public int[][] merge(int[][] intervals) {
    if (intervals.length <= 1) return intervals;
    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
    List<int[]> out = new ArrayList<>();
    out.add(intervals[0]);
    for (int i = 1; i < intervals.length; i++) {
        int[] lastMerged = out.get(out.size() - 1);
        int s = intervals[i][0];
        int e = intervals[i][1];
        if (s <= lastMerged[1]) {
            lastMerged[1] = Math.max(lastMerged[1], e); // extend current interval
        } else {
            out.add(new int[]{s, e});
        }
    }
    return out.toArray(new int[out.size()][]);
}
C++
// C++ — Merge overlapping intervals
#include <vector>
#include <algorithm>

std::vector<std::vector<int>> merge(std::vector<std::vector<int>>& intervals) {
    if (intervals.size() <= 1) return intervals;
    std::sort(intervals.begin(), intervals.end(), [](const auto& a, const auto& b) {
        return a[0] < b[0];
    });
    std::vector<std::vector<int>> out;
    out.push_back(intervals[0]);
    for (size_t i = 1; i < intervals.size(); i++) {
        if (intervals[i][0] <= out.back()[1]) {
            out.back()[1] = std::max(out.back()[1], intervals[i][1]); // extend
        } else {
            out.push_back(intervals[i]);
        }
    }
    return out;
}

For "max concurrent intervals" (meeting rooms), sweep the sorted start/end events or use a min-heap of end times. Sort by end time for greedy "max non-overlapping intervals".

Think it through

This chapter has two ideas — a heap for "current extreme of a changing set", and sort-then-sweep for intervals. One classic problem needs both. Reason through it before revealing.

Think it through: Meeting Rooms IIMedium — LeetCode 2530/5 stages

PROBLEMGiven meeting time intervals, return the minimum number of rooms so that no two overlapping meetings share a room. meetings = [[0,30],[5,10],[15,20]] → 2.

  1. 1

    Restate & edges

    Rephrase 'minimum rooms' as a counting question.

  2. 2

    Brute force first

    Obvious solution and its cost?

    unlocks after the stage above
  3. 3

    Find the pattern

    Sort by start. As each meeting begins, what one fact decides 'new room or reuse'?

    unlocks after the stage above
  4. 4

    Code the template

    Why does returning len(ends) give the PEAK, not just the final, room count?

    unlocks after the stage above
  5. 5

    Cost & edge check

    Cost, and trace the example?

    unlocks after the stage above
Java
// Java — Meeting Rooms II
import java.util.Arrays;
import java.util.PriorityQueue;

public int minMeetingRooms(int[][] meetings) {
    if (meetings == null || meetings.length == 0) return 0;
    Arrays.sort(meetings, (a, b) -> Integer.compare(a[0], b[0]));
    PriorityQueue<Integer> ends = new PriorityQueue<>(); // min-heap of end times
    for (int[] m : meetings) {
        if (!ends.isEmpty() && ends.peek() <= m[0]) {
            ends.poll(); // reuse room
        }
        ends.add(m[1]);
    }
    return ends.size(); // heap size only grows to match peak concurrency
}
C++
// C++ — Meeting Rooms II
#include <vector>
#include <queue>
#include <algorithm>

int minMeetingRooms(std::vector<std::vector<int>>& meetings) {
    if (meetings.empty()) return 0;
    std::sort(meetings.begin(), meetings.end(), [](const auto& a, const auto& b) {
        return a[0] < b[0];
    });
    // min-heap of end times
    std::priority_queue<int, std::vector<int>, std::greater<int>> ends;
    for (const auto& m : meetings) {
        if (!ends.empty() && ends.top() <= m[0]) {
            ends.pop(); // reuse room
        }
        ends.push(m[1]);
    }
    return ends.size(); // peak concurrency
}

Check yourself

Check yourself0/4 answered

1. To find the K LARGEST elements efficiently, you keep a:

2. Why is a size-K heap the only top-K approach that works on a STREAM?

3. Sorting is the first move on most interval problems. You sort by END time when you want to:

4. Meeting Rooms II: the minimum number of rooms equals:

Practice — climb the ladder

Heap trigger words: "k largest", "k closest", "median of a stream". Interval trigger: sort by start (merge) or by end (scheduling), then sweep.

Practice ladder: Heaps & Intervals0/9 solved

Climb in order — every rung assumes the one above it. Solve on LeetCode, then tick it here; progress is saved on this device.

Warm-up

heap API fluency
  1. A min-heap of size k — the counterintuitive trick behind every top-k problem.

  2. Repeated extract-max — pure heap mechanics, nothing else in the way.

Core

top-k + the two interval sorts
  1. Heap of size k vs quickselect — know both and say the trade-off.

  2. Top-k with a comparator — same skeleton, custom distance.

  3. Sort by START, extend the current block — the merge half of interval thinking.

  4. Sort by END, keep what finishes earliest — the greedy scheduling half.

  5. Greedy by frequency with a heap — capacity math meets extract-max.

Stretch

two heaps, opposing directions
  1. Max-heap + min-heap balanced around the middle — the two-heap pattern.

  2. Sort-by-end greedy again — proof you recognize the pattern in costume.