{}DSA Atlas

Heaps & Priority Queues

Always know the smallest (or largest) item in O(1) and update in O(log n). Top-K, K-way merge, two heaps for medians, and scheduling simulations.

Intermediate14 practice problems2 easy8 medium4 hard

What a heap is

A binary heap is a complete binary tree stored in an array where every parent is ≤ its children (min-heap) or ≥ (max-heap). So the root is always the minimum (or maximum).

OperationCost
peek min/maxO(1)
pushO(log n)
pop min/maxO(log n)
build from n items (heapify)O(n)
search / delete arbitrary itemO(n) (unless indexed)

Array layout (0-indexed): children of i are 2i + 1 and 2i + 2; parent is (i − 1) // 2.

Using heaps in your language

import heapq

h = []
heapq.heappush(h, 5)
heapq.heappush(h, 1)
smallest = h[0]             # peek
heapq.heappop(h)            # pop min
heapq.heapify(arr)          # O(n) in place
heapq.heappushpop(h, x)     # push then pop (efficient)
heapq.heapreplace(h, x)     # pop then push
heapq.nlargest(k, arr)      # convenience, O(n log k)

# max-heap: negate values
heapq.heappush(h, -x); largest = -h[0]

# tuples compare lexicographically: (priority, tie_breaker, item)
heapq.heappush(h, (dist, node))
#include <queue>

priority_queue<int> maxHeap;                              // max-heap by default!
priority_queue<int, vector<int>, greater<int>> minHeap;   // min-heap
maxHeap.push(5); maxHeap.top(); maxHeap.pop();

// pairs: compared by first, then second
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;

// custom comparator (returns true if a has LOWER priority than b)
auto cmp = [](const Node& a, const Node& b) { return a.cost > b.cost; };  // min by cost
priority_queue<Node, vector<Node>, decltype(cmp)> pq2(cmp);

How it works inside

Python
class MinHeap:
    def __init__(self):
        self.a = []

    def push(self, x):
        a = self.a
        a.append(x)
        i = len(a) - 1
        while i > 0 and a[(i - 1) // 2] > a[i]:          # sift up
            p = (i - 1) // 2
            a[i], a[p] = a[p], a[i]
            i = p

    def pop(self):
        a = self.a
        top = a[0]
        last = a.pop()
        if a:
            a[0] = last
            i, n = 0, len(a)
            while True:                                   # sift down
                l, r, m = 2 * i + 1, 2 * i + 2, i
                if l < n and a[l] < a[m]: m = l
                if r < n and a[r] < a[m]: m = r
                if m == i: break
                a[i], a[m] = a[m], a[i]
                i = m
        return top

Heapify is O(n) (not O(n log n)) because most nodes are near the bottom and sift down only a little.

Pattern 1: Top K with a bounded heap

To keep the K largest, use a min-heap of size K: the root is the smallest of the top K, and anything smaller than it can be discarded.

import heapq

def k_largest(nums, k):
    h = []
    for x in nums:
        heapq.heappush(h, x)
        if len(h) > k:
            heapq.heappop(h)        # drop the smallest
    return h                         # h[0] is the k-th largest
int findKthLargest(vector<int>& nums, int k) {
    priority_queue<int, vector<int>, greater<int>> h;
    for (int x : nums) {
        h.push(x);
        if ((int)h.size() > k) h.pop();
    }
    return h.top();
}

O(n log k) time, O(k) space. The rule of thumb: K largest → min-heap; K smallest → max-heap (the opposite of what you might expect).

Top K frequent

Python
from collections import Counter
import heapq

def top_k_frequent(nums, k):
    count = Counter(nums)
    return heapq.nlargest(k, count.keys(), key=count.get)

# O(n) bucket sort alternative: frequencies are bounded by n
def top_k_frequent_bucket(nums, k):
    count = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]
    for x, c in count.items():
        buckets[c].append(x)
    res = []
    for c in range(len(buckets) - 1, 0, -1):
        res.extend(buckets[c])
        if len(res) >= k:
            return res[:k]

Pattern 2: K-way merge

Merge K sorted sequences by keeping one candidate per sequence in a heap.

import heapq

def merge_k_lists(lists):
    h = []
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(h, (node.val, i, node))   # i breaks ties (nodes aren't comparable)
    dummy = tail = ListNode()
    while h:
        _, i, node = heapq.heappop(h)
        tail.next = tail = node
        if node.next:
            heapq.heappush(h, (node.next.val, i, node.next))
    return dummy.next
ListNode* mergeKLists(vector<ListNode*>& lists) {
    auto cmp = [](ListNode* a, ListNode* b) { return a->val > b->val; };
    priority_queue<ListNode*, vector<ListNode*>, decltype(cmp)> pq(cmp);
    for (auto l : lists) if (l) pq.push(l);
    ListNode dummy, *tail = &dummy;
    while (!pq.empty()) {
        ListNode* n = pq.top(); pq.pop();
        tail->next = n; tail = n;
        if (n->next) pq.push(n->next);
    }
    return dummy.next;
}

O(N log K) for N total elements. The same idea generates the k smallest pair sums or the k-th smallest in a sorted matrix: treat each row as a sorted list and lazily push the next candidate.

Pattern 3: Two heaps (running median)

Keep the smaller half in a max-heap and the larger half in a min-heap, sizes balanced within 1. The median is at the tops.

import heapq

class MedianFinder:
    def __init__(self):
        self.low = []    # max-heap (negated)
        self.high = []   # min-heap

    def addNum(self, x):
        heapq.heappush(self.low, -x)
        heapq.heappush(self.high, -heapq.heappop(self.low))   # move the largest low to high
        if len(self.high) > len(self.low):
            heapq.heappush(self.low, -heapq.heappop(self.high))

    def findMedian(self):
        if len(self.low) > len(self.high):
            return -self.low[0]
        return (-self.low[0] + self.high[0]) / 2
class MedianFinder {
    priority_queue<int> low;                              // max-heap
    priority_queue<int, vector<int>, greater<int>> high;  // min-heap
public:
    void addNum(int x) {
        low.push(x);
        high.push(low.top()); low.pop();
        if (high.size() > low.size()) { low.push(high.top()); high.pop(); }
    }
    double findMedian() {
        if (low.size() > high.size()) return low.top();
        return (low.top() + (double)high.top()) / 2;
    }
};

Two heaps also solve IPO (available projects vs locked ones) and scheduling problems where items move from “waiting” to “ready”.

Pattern 4: Simulation / event scheduling

Process events in time order when new events are created on the fly.

Python
import heapq

def get_order(tasks):                          # Single-Threaded CPU
    order = sorted(range(len(tasks)), key=lambda i: tasks[i][0])
    ready, res, time, i = [], [], 0, 0
    while len(res) < len(tasks):
        if not ready and time < tasks[order[i]][0]:
            time = tasks[order[i]][0]          # CPU idle: jump ahead
        while i < len(order) and tasks[order[i]][0] <= time:
            j = order[i]
            heapq.heappush(ready, (tasks[j][1], j))
            i += 1
        dur, j = heapq.heappop(ready)
        time += dur
        res.append(j)
    return res

Dijkstra and Prim are also “always expand the cheapest frontier item” — see Shortest Paths.

Lazy deletion

Heaps can't delete arbitrary items efficiently. Instead, mark items as deleted (in a hash map of pending removals or by a version/timestamp) and discard them when they reach the top.

Python
while heap and is_stale(heap[0]):
    heapq.heappop(heap)

Used in Sliding Window Median, the Skyline Problem, and Dijkstra (skip entries whose distance is outdated).

Heap vs other structures

NeedUse
repeated min/max with insertsheap
min/max + delete arbitrary + ordered iterationbalanced BST (std::set, SortedList)
k-th element once, no updatesquickselect O(n)
min/max of a sliding windowmonotonic deque O(n)
everything sorted oncejust sort

Common mistakes

  • Min vs max heap confusion between languages.
  • Comparing non-comparable objects in Python tuples (add an index tie-breaker).
  • Forgetting heap order is not sorted order — only h[0] is guaranteed.
  • Using a heap where sorting once is simpler (no inserts after the start).
  • Rebuilding a heap each iteration (O(n) each) instead of pushing/popping.

Practice problems

Ordered roughly from warm-up to hard. Try each one for 25–40 minutes before peeking at the key idea; if you needed the hint, mark it “review” and redo it in a few days.

0 / 14 solved
Min-heap of size k; the root is the k-th largest.
Easy
Max-heap simulation: pop two, push the difference if non-zero.
Easy
Size-k min-heap: O(n log k). Or quickselect.
Medium
Count, then heap by frequency (or bucket sort).
Medium
Max-heap of size k keyed by squared distance.
Medium
Max-heap of counts + cooldown queue simulation (or the counting formula).
Medium
Always place the most frequent char that isn't the previous one; hold the previous aside for one step.
Medium
Heap seeded with (nums1[i] + nums2[0]); popping (i, j) pushes (i, j+1).
Medium
K-way merge of rows with a heap, or binary search on value with a staircase count.
Medium
Sort by enqueue time; heap of available tasks by (processing time, index); jump time when idle.
Medium
Heap of (value, list index, node).
Hard
Max-heap for the low half, min-heap for the high half; rebalance so sizes differ by at most 1.
Hard
Two heaps with lazy deletion (hash map of pending removals), or a sorted list.
Hard
Heap holds one element per list; track the current max; pop min and advance that list.
Hard

Click the box to cycle: solved ✓ → needs review ↺ → not started. Links open LeetCode.