Skip to content

Heap & Priority Queue: Practice Problems with Solutions

Welcome to the practice problems for heap and priority queue. If you need a refresher on the code, the code templates have the patterns in all 6 languages. Each problem below includes a hint, a visual walkthrough, and the full solution.

The Heap Helpers for JavaScript and Ruby

Python, Java, C++, and Go have standard heaps, so the solutions below use them directly. JavaScript and Ruby do not, so every JavaScript and Ruby solution on this page uses the Heap helper class from the code templates . In short, new Heap() is a min-heap, and a comparator like (a, b) => a > b (JavaScript) or { |a, b| a > b } (Ruby) turns it into a max-heap. The solutions below only show the problem logic.

Recommended Study Order

Start with Last Stone Weight. It is the smallest possible heap program. You build a max-heap and then keep popping. Kth Largest Element in a Stream adds the size cap, which is the idea behind every top-K problem. Kth Largest Element in an Array applies the same cap to a static input. Top K Frequent Elements adds a second key, frequency, which is where custom comparators start mattering. K Closest Points to Origin compares by distance instead of raw value. Merge k Sorted Lists is the first problem where the heap holds the next candidate from each list instead of a finished value. Find Median from Data Stream ends the set by combining a min-heap and a max-heap into one structure. If you can explain why the two median heaps stay balanced, you understand heaps.

The order above is designed to build intuition progressively. The app schedules your reviews so you do not forget the Easy patterns while grinding Hard ones. Set up your review schedule .

Easy Problems

1. Last Stone Weight

LeetCode 1046 | Difficulty: Easy

Brief: Smash the two heaviest stones together. If they weigh the same, both are destroyed. Otherwise the difference goes back into the pile. Return the weight of the last stone, or 0 if nothing is left.

Why this pattern: The problem repeatedly needs the two largest elements in a collection that keeps changing. A max-heap serves both pops and the occasional re-insert in

O(log N)
each.

Key Insight: Every iteration runs the same three steps. Pop twice, then push the difference if it is non-zero. The heap keeps the heaviest stone at the root, so the two pops never need to scan the pile.

Complexity: Time

O(N log N)
, Space
O(N)

Visual:

    graph TD
    A["Stones: 2, 7, 4, 1, 8, 1"] --> B["Pop 8 and 7, push 1"]
    B --> C["Stones: 2, 4, 1, 1, 1"]
    C --> D["Pop 4 and 2, push 2"]
    D --> E["Stones: 2, 1, 1, 1"]
    E --> F["Pop 2 and 1, push 1"]
    F --> G["Pop 1 and 1, both destroyed"]
    G --> H["Last stone: 1"]
  

Code:

var lastStoneWeight = function(stones) {
    // max-heap: the root holds the heaviest stone
    const heap = new Heap(stones, (a, b) => a > b);

    while (heap.size() > 1) {
        const heaviest = heap.pop();
        const second = heap.pop();
        if (heaviest !== second) {
            heap.push(heaviest - second);
        }
    }

    return heap.size() === 0 ? 0 : heap.peek();
};

The solution is the max-heap template with no twists. Building the heap with a batch operation costs

O(N)
, and each of the at most N smashes costs two pops and one push. The only edge case is the final read. When the heap empties out, the answer is 0, not an error.

2. Kth Largest Element in a Stream

LeetCode 703 | Difficulty: Easy

Brief: Design a class that receives a stream of integers. Every add call inserts one value and returns the kth largest element of everything seen so far.

Why this pattern: The size-k min-heap is the exact shape of this problem. The heap holds the k largest values seen so far, and its root is the answer.

Key Insight: The constructor shrinks the initial heap to size k once. Every add then does push, pop-if-overflow, and read the root. No scanning, no sorting, no memory that grows with the stream.

Complexity: Time

O(log k)
per add, Space
O(k)

Visual:

    graph TD
    A["k = 3, heap keeps size 3"] --> B["Add 3"]
    B --> C["Heap: 4, 5, 8, return 4"]
    C --> D["Add 5"]
    D --> E["Heap: 5, 5, 8, return 5"]
    E --> F["Add 10"]
    F --> G["Heap: 5, 8, 10, return 5"]
    G --> H["Add 9"]
    H --> I["Heap: 8, 9, 10, return 8"]
  

Code:

var KthLargest = function(k, nums) {
    this.k = k;
    // min-heap capped at k: the root is the kth largest
    this.heap = new Heap(nums);
    while (this.heap.size() > k) {
        this.heap.pop();
    }
};

KthLargest.prototype.add = function(val) {
    this.heap.push(val);
    if (this.heap.size() > this.k) {
        this.heap.pop();
    }
    return this.heap.peek();
};

This problem is the size-k template in class form. The constructor does the shrink once, and add repeats the push-then-trim cycle forever. The space stays at

O(k)
no matter how long the stream runs, which is the property that makes the design acceptable for live data.

Medium Problems

3. Kth Largest Element in an Array

LeetCode 215 | Difficulty: Medium

Brief: Return the kth largest element of an unsorted array.

Why this pattern: The same size-k min-heap works on a static array. Sort the whole array and you pay

O(N log N)
. The capped heap pays
O(N log k)
and never stores more than k values.

Key Insight: After every insert, pop the root if the heap holds more than k elements. The root is then the kth largest of everything processed, which is exactly the answer when the array is exhausted.

Complexity: Time

O(N log k)
, Space
O(k)

Visual:

    graph TD
    A["nums: 3, 2, 1, 5, 6, 4, k = 2"] --> B["Push 3, 2"]
    B --> C["Push 1, size > 2, pop 1"]
    C --> D["Push 5, pop 2"]
    D --> E["Push 6, pop 3"]
    E --> F["Push 4, pop 4"]
    F --> G["Root: 5, the 2nd largest"]
  

Code:

var findKthLargest = function(nums, k) {
    // min-heap capped at k keeps the k largest values.
    // the root is the kth largest of everything seen.
    const heap = new Heap();
    for (const num of nums) {
        heap.push(num);
        if (heap.size() > k) {
            heap.pop();
        }
    }
    return heap.peek();
};

This is the stream version from the previous problem with the stream replaced by a loop. The guaranteed worst case is what distinguishes the heap here. Quickselect, the divide and conquer alternative covered on the divide and conquer page , has expected

O(N)
time but can degrade to
O(N^2)
with unlucky pivots. The heap never degrades.

4. Top K Frequent Elements

LeetCode 347 | Difficulty: Medium

Brief: Return the k most frequent elements from an array.

Why this pattern: Count frequencies first, then the top-K template applies with the frequency as the heap key instead of the raw value.

Key Insight: The heap stores (count, element) pairs and caps at k. The root is the least frequent element among the survivors, so it is the one that leaves when a more frequent element arrives.

Complexity: Time

O(N log k)
, Space
O(N)

Visual:

    graph TD
    A["nums: 1, 1, 1, 2, 2, 3"] --> B["freq: 1 has 3, 2 has 2, 3 has 1"]
    B --> C["k = 2"]
    C --> D["Push (3, 1), push (2, 2)"]
    D --> E["Push (1, 3), size > 2, pop (1, 3)"]
    E --> F["Return 1 and 2"]
  

Code:

var topKFrequent = function(nums, k) {
    const freq = new Map();
    for (const num of nums) {
        freq.set(num, (freq.get(num) || 0) + 1);
    }

    // pairs [count, num] in a min-heap; the root is the
    // least frequent element among the current k
    const heap = new Heap([], (a, b) => a[0] < b[0]);
    for (const [num, count] of freq) {
        heap.push([count, num]);
        if (heap.size() > k) {
            heap.pop();
        }
    }

    const result = [];
    while (heap.size() > 0) {
        result.push(heap.pop()[1]);
    }
    return result;
};

The frequency map costs

O(N)
space, which is unavoidable because every distinct value appears in it. After the count pass, the heap work is the same as any top-K problem. The result order does not matter, since the problem accepts any ordering of the k answers.

5. K Closest Points to Origin

LeetCode 973 | Difficulty: Medium

Brief: Given an array of points, return the k closest points to the origin.

Why this pattern: k closest means k smallest distances, so the max-heap variation applies. The heap caps at k, and the root holds the largest distance among the survivors.

Key Insight: Compare by squared distance, not the raw distance. The square root is monotonic, so it cannot change the ordering, and skipping it avoids floating point entirely.

Complexity: Time

O(N log k)
, Space
O(k)

Visual:

    graph TD
    A["points: (1,3), (-2,2), (5,-1), (3,4)"] --> B["dists: 10, 8, 26, 25"]
    B --> C["k = 2"]
    C --> D["(5,-1) at distance 26 is popped"]
    D --> E["(3,4) at distance 25 is popped"]
    E --> F["Survivors: (1,3) and (-2,2)"]
  

Code:

var kClosest = function(points, k) {
    const dist = (p) => p[0] * p[0] + p[1] * p[1];

    // max-heap by distance keeps the k closest points. the
    // root is the farthest survivor and leaves when a
    // closer point arrives.
    const heap = new Heap([], (a, b) => dist(a) > dist(b));
    for (const p of points) {
        heap.push(p);
        if (heap.size() > k) {
            heap.pop();
        }
    }

    const result = [];
    while (heap.size() > 0) {
        result.push(heap.pop());
    }
    return result;
};

The squared distance trick is worth stating out loud in an interview. It removes the square root, keeps the comparison exact with integers, and changes nothing about the answer. The custom comparator is the only new piece compared to the plain top-K template.

Hard Problems

6. Merge k Sorted Lists

LeetCode 23 | Difficulty: Hard

Brief: Merge k sorted linked lists into one sorted linked list.

Why this pattern: The min-heap holds the current head of every list. The smallest head in the heap is always the next element of the merged result.

Key Insight: When a head is popped, the next node from that same list enters the heap. This keeps the heap at exactly k nodes and ensures the smallest available element is always at the root.

Complexity: Time

O(N log k)
, Space
O(k)

Visual:

    graph TD
    A["Heads 1, 1, 2 enter the min-heap"] --> B["Pop 1, push that list's next: 4"]
    B --> C["Pop 1, push that list's next: 3"]
    C --> D["Pop 2, push that list's next: 6"]
    D --> E["Heap holds 3, 4, 6"]
    E --> F{"Heap empty?"}
    F -->|No| B
    F -->|Yes| G["Result: 1, 1, 2, 3, 4, 4, 5, 6"]
  

Code:

var mergeKLists = function(lists) {
    class ListNode {
        constructor(val = 0, next = null) {
            this.val = val;
            this.next = next;
        }
    }

    // min-heap of list nodes ordered by value
    const heap = new Heap([], (a, b) => a.val < b.val);
    for (const head of lists) {
        if (head) {
            heap.push(head);
        }
    }

    const dummy = new ListNode();
    let current = dummy;
    while (heap.size() > 0) {
        current.next = heap.pop();
        current = current.next;
        if (current.next) {
            heap.push(current.next);
        }
    }
    return dummy.next;
};

The dummy node removes the special case of the empty result, the same trick as in linked list reversal problems. The pairwise merge alternative, which pairs lists and merges two at a time, appears on the divide and conquer page . Both are

O(N log k)
; the heap version keeps the k heads in one structure and is the answer most interviewers expect to hear.

7. Find Median from Data Stream

LeetCode 295 | Difficulty: Hard

Brief: Design a class that supports adding a number and finding the median of all numbers added so far.

Why this pattern: The two-heap split keeps the lower half in a max-heap and the upper half in a min-heap. The median is always at the boundary between the two roots.

Key Insight: Every insert pushes into the lower half, moves the largest lower value into the upper half, then moves it back if the sizes drift. After the dance, the lower half holds the extra element when the total count is odd, and the median is a root read either way.

Complexity: Time

O(log N)
per add,
O(1)
per median read, Space
O(N)

Visual:

    graph TD
    A["Add 1"] --> B["low: [1], high: []"]
    B --> C["Add 2"]
    C --> D["low: [1], high: [2]"]
    D --> E["Add 3"]
    E --> F["low: [1, 2], high: [3]"]
    F --> G["Median is the low root: 2"]
  

Code:

var MedianFinder = function() {
    // low is a max-heap, high is a min-heap
    this.low = new Heap([], (a, b) => a > b);
    this.high = new Heap();
};

MedianFinder.prototype.addNum = function(num) {
    // push into low, then move low's largest into high.
    // this keeps every low value below every high value.
    this.low.push(num);
    this.high.push(this.low.pop());
    // low holds the extra element when the count is odd
    if (this.low.size() < this.high.size()) {
        this.low.push(this.high.pop());
    }
};

MedianFinder.prototype.findMedian = function() {
    if (this.low.size() > this.high.size()) {
        return this.low.peek();
    }
    return (this.low.peek() + this.high.peek()) / 2;
};

The two-step insert reads strangely at first, but the order matters. Pushing into low first and then moving low’s largest into high guarantees the ordering invariant, because low’s largest is always smaller than or equal to anything already in high. The rebalance step then repairs any size drift. Trace the insert with an even count and an odd count and the median read becomes obviously correct. Odd counts use the low root, and even counts average the two roots.

Hard problems are where patterns separate candidates who pass from candidates who get offers. Track your misses and review them on a spaced schedule so you do not repeat them in the real interview.

These seven problems cover the full range of heap applications. Start with the extraction loop in Last Stone Weight, internalize the size cap through the three top-K problems, and finish with the two-heap balance in Find Median from Data Stream. By the end, you should be able to look at a problem and say immediately which extreme needs to sit at the root, and which of the templates supplies it.

Done with these problems? The app has more, plus a spaced repetition system that brings problems back right before you would forget them. Continue your prep .