Skip to content
Commitwise
Menu
Coding Interview

Heaps

Use priority queues to always get the smallest or largest element fast, with Kth Largest Element, Top K Frequent Elements, merging sorted lists and Find Median from Data Stream in Swift, Kotlin, Java and TypeScript.

intermediate

The core idea with one simple example.Step by step, with more examples and exercises.A cheat sheet: key APIs, gotchas and trade-offs.Internals, trade-offs and production pitfalls.

“The 10 highest scores.” “The next task to run.” “The 3 closest restaurants.” “The median of a live stream of response times.” Sorting everything answers the question, but costs O(n log n) every time something changes. A heap answers “what’s the smallest (or largest) right now?” instantly, and absorbs a new element in O(log n).

Heaps are behind priority queues, schedulers and timers, and they turn a whole family of “top k” interview problems from O(n log n) into O(n log k).

New to some of these? Open what you need, or skip ahead.

Basics:What are lists, sets and maps?

Collections hold many values:

  • A list (Swift Array, Kotlin List, Java List, a TypeScript array) keeps items in order, and allows duplicates: ["Ana", "Ben", "Ana"].
  • A set holds each value at most once and answers “is this in it?” very quickly.
  • A map (Swift Dictionary, Kotlin, Java and TypeScript Map) stores values under keys: "Ana" → 30, looked up by the key.

Lists use indexes starting at 0: in ["a", "b", "c"], "a" is at index 0 and "c" at index 2.

Basics:What is a comparator?

To sort or rank things, code needs to know which of two items comes first. A comparator answers exactly that question: given a and b, is a before, after, or equal to b?

Swift:   people.sorted { $0.age < $1.age }
Kotlin:  people.sortedBy { it.age }            // or compareBy { it.age }
Java:    people.sort(Comparator.comparingInt(Person::age));
TS:      people.toSorted((a, b) => a.age - b.age)

Types with a natural order (numbers, strings) are comparable already; for your own types, you provide the rule. Priority queues, sorted maps and max/min all rely on it.

Basics:What does O(n) mean?

Big-O describes how the amount of work grows with the size of the input n:

  • O(1): the same work whatever the size (reading array[5]).
  • O(log n): grows very slowly (about 20 steps for a million items).
  • O(n): grows in step with the input (one pass over the array).
  • O(n²): comparing every item with every other item, which gets slow fast.

You’ll see it next to every solution, for time and for extra space (memory). The Big-O tutorial explains it properly.

Part Not started

Always know the smallest

Think of an emergency room. Patients don’t wait in arrival order: the most urgent case is always treated next. New patients arrive all the time, and each time, the staff just needs to know who is most urgent right now. They don’t need the full list in order.

A heap (in code, usually a priority queuePriority queueA collection that always hands out the smallest (or largest) element next, usually built on a heap.Open in glossary →) is built for exactly that:

  • peek at the smallest element (a min-heap) or the largest (a max-heap): O(1),
  • add an element: O(log n),
  • remove the smallest (or largest): O(log n).

It doesn’t keep everything sorted, which is why it’s faster than sorting: it only keeps enough order to know the top.

A binary heap is a complete binary tree stored in an array, satisfying the heap property (each parent ≤ its children for a min-heap). Insert and extract are O(log n) sift-up/sift-down operations, peek is O(1), and building a heap from n elements (heapify) is O(n). The interview patterns are bounded heaps for top-k (keep k elements, O(n log k)), k-way merges (heap of the current head of each list), two heaps for running medians, and lazy deletion when elements must be removed from the middle. The Detailed version adds internals, the iteration-order trap and comparator bugs.

import HeapModule                        // swift-collections: the standard library has no heap

var heap = Heap<Int>()
for value in [5, 1, 8, 3] { heap.insert(value) }
print(heap.min!)                         // 1: peek at the smallest
print(heap.popMin()!)                    // 1: remove it
print(heap.popMax()!)                    // 8: Heap is a min-max heap, so both ends are cheap
val heap = PriorityQueue<Int>()          // min-heap by default
listOf(5, 1, 8, 3).forEach { heap.add(it) }
println(heap.peek())                     // 1: peek at the smallest
println(heap.poll())                     // 1: remove it

val maxHeap = PriorityQueue<Int>(compareByDescending { it })   // or reverseOrder()
PriorityQueue<Integer> heap = new PriorityQueue<>();       // min-heap by default
for (int value : List.of(5, 1, 8, 3)) heap.add(value);
System.out.println(heap.peek());                           // 1: peek at the smallest
System.out.println(heap.poll());                           // 1: remove it

PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
// No built-in heap: a small binary heap with a comparator (or a package)
class Heap<T> {
  private readonly items: T[] = [];
  constructor(private readonly before: (a: T, b: T) => boolean) {}   // true if a belongs above b

  get size() { return this.items.length; }
  get unordered(): readonly T[] { return this.items; }               // the backing array, heap order
  peek(): T | undefined { return this.items[0]; }

  push(value: T) {
    const a = this.items;
    a.push(value);
    let i = a.length - 1;
    while (i > 0) {                                                  // sift up
      const parent = (i - 1) >> 1;
      if (!this.before(a[i], a[parent])) break;
      [a[i], a[parent]] = [a[parent], a[i]];
      i = parent;
    }
  }

  pop(): T | undefined {
    const a = this.items;
    const top = a[0];
    const last = a.pop();
    if (a.length > 0 && last !== undefined) {
      a[0] = last;
      let i = 0;
      while (true) {                                                 // sift down
        const l = 2 * i + 1;
        const r = l + 1;
        let best = i;
        if (l < a.length && this.before(a[l], a[best])) best = l;
        if (r < a.length && this.before(a[r], a[best])) best = r;
        if (best === i) break;
        [a[i], a[best]] = [a[best], a[i]];
        i = best;
      }
    }
    return top;
  }
}

const heap = new Heap<number>((a, b) => a < b);      // a min-heap
for (const value of [5, 1, 8, 3]) heap.push(value);
console.log(heap.peek());                            // 1: peek at the smallest
console.log(heap.pop());                             // 1: remove it

const maxHeap = new Heap<number>((a, b) => a > b);

Each language has a different starting point:

  • Kotlin and Java use java.util.PriorityQueue, a min-heap by default. For a max-heap, pass a reversed comparator.
  • Swift has no heap in the standard library; Apple’s swift-collections package provides Heap, which supports both min and max efficiently.
  • TypeScript has no heap at all: write a small one like the class above, or use a package.
  • In interviews, it’s fine to say “assume a priority queue” in Swift or TypeScript and focus on the algorithm.

How a heap stays fast

A binary heap is a tree where every parent is smaller than its children (for a min-heap), stored in a plain array: the children of index i are at 2i + 1 and 2i + 2, and its parent at (i − 1) / 2.

  • Adding: put the new value at the end of the array, then swap it with its parent while it’s smaller (“sift up”). At most one swap per level: O(log n).
  • Removing the minimum: take the root, move the last element to the root, then swap it down with its smaller child while it’s bigger (“sift down”): O(log n).

The tree is always as balanced as possible, so it’s about log₂(n) levels deep: 20 for a million elements.

Internals and their consequences

  • Iteration order isn’t sorted: iterating or printing a PriorityQueue shows the backing array, which satisfies only the heap property (the puzzle). To get sorted output, poll repeatedly.
  • Heapify is O(n): building from a collection (PriorityQueue(collection), Heap(array)) sifts down from the middle and is linear, faster than n inserts at O(n log n).
  • No efficient removal of arbitrary elements: PriorityQueue.remove(x) is O(n). Algorithms that need it (sliding window medians, cancelled timers) use lazy deletion: mark the element as deleted (a hash map of pending deletions), and discard it when it reaches the top.
  • Comparators define everything: they must be consistent and must not overflow (the bug below). A comparator on a mutable field breaks the heap if that field changes while the element is inside.
  • Where heaps appear in production: ScheduledThreadPoolExecutor and timer wheels, Dijkstra and A* in routing, top-k aggregation in analytics, and k-way merges in databases and log processing.

Printing a priority queue

Predict the output

3, 1 and 2 are added to a min-heap. What does printing its contents show?

var heap = Heap<Int>()
for value in [3, 1, 2] { heap.insert(value) }
print(Array(heap.unordered))
val heap = PriorityQueue<Int>()
listOf(3, 1, 2).forEach { heap.add(it) }
println(heap)
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int value : List.of(3, 1, 2)) heap.add(value);
System.out.println(heap);
const heap = new Heap<number>((a, b) => a < b);
for (const value of [3, 1, 2]) heap.push(value);
console.log(heap.unordered);

Deep dives

Deep diveWhy heapify is O(n)Interview favorite

Building a heap with n inserts costs O(n log n). Building it in place from an array, by sifting down every node from the middle back to the root, costs only O(n), which surprises people.

The reason: sift-down costs the node’s height, and most nodes are near the bottom. Half the nodes are leaves (height 0, no work), a quarter have height 1, an eighth height 2, and so on. The total is n · (0/2 + 1/4 + 2/8 + 3/16 + …), and that sum converges to n. That’s why PriorityQueue(collection) and Heap(array) are faster than adding elements one by one.

OperationCostSwift (HeapModule)Kotlin / Java (PriorityQueue)TypeScript (your own Heap or a package)
peek min / maxO(1)min, maxpeek() (min by default)peek() (order set by the comparator)
insertO(log n)insert(_:)add / offerpush
remove topO(log n)popMin(), popMax()poll()pop()
build from n itemsO(n)Heap(array)PriorityQueue(collection)heapify by hand, or n pushes (O(n log n))
remove arbitraryO(n)—remove(x); prefer lazy deletionlazy deletion

Patterns: top-k with a bounded heap of the opposite kind (min-heap for largest), k-way merge (heap of heads), two heaps for medians, scheduling by time or priority, Dijkstra. Gotchas: default is a min-heap, iteration isn’t sorted, comparators that subtract overflow, mutating a field the comparator uses, Integer sums of two tops overflow.

Check this part

  1. What does a min-heap give you in O(1)?

  2. Where does a Swift heap come from?What is PriorityQueue with no comparator?Where does a TypeScript heap come from?

  3. Printing a heap after adding 3, 1, 2 shows…

  4. What does building a heap from n existing elements (heapify) cost?

A heap peeks at the smallest (or largest) element in O(1) and adds or removes it in O(log n), by keeping only enough order to know the top. It's a tree stored in an array, and iterating it isn't sorted.

Part Not started

The k largest

The classic question: return the k-th largest element of an array, for example 5 for [3, 2, 1, 5, 6, 4] and k = 2. Sorting works in O(n log n), but you only need the k largest values seen so far, and which of them is the smallest. That’s a min-heap of size k: add each number, and when the heap grows beyond k, remove its minimum.

func findKthLargest(_ nums: [Int], _ k: Int) -> Int {
    var heap = Heap<Int>()
    for n in nums {
        heap.insert(n)
        if heap.count > k { heap.removeMin() }     // drop the smallest: it can't be in the top k
    }
    return heap.min!
}
fun findKthLargest(nums: IntArray, k: Int): Int {
    val heap = PriorityQueue<Int>()
    for (n in nums) {
        heap.add(n)
        if (heap.size > k) heap.poll()             // drop the smallest: it can't be in the top k
    }
    return heap.peek()
}
int findKthLargest(int[] nums, int k) {
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    for (int n : nums) {
        heap.add(n);
        if (heap.size() > k) heap.poll();          // drop the smallest: it can't be in the top k
    }
    return heap.peek();
}
function findKthLargest(nums: number[], k: number): number {
  const heap = new Heap<number>((a, b) => a < b);   // min-heap
  for (const n of nums) {
    heap.push(n);
    if (heap.size > k) heap.pop();                  // drop the smallest: it can't be in the top k
  }
  return heap.peek()!;
}

It feels backwards at first: to find the largest values, use a min-heap. The heap is a bouncer at a club with k spots: when it’s full, the weakest member leaves. With k much smaller than n, O(n log k) beats sorting, and the heap never holds more than k elements, so it works on a stream that doesn’t fit in memory.

The wrong end of the heap

Spot the bugTop three scores that are the bottom three

Tap the line with the bug.

func topThreeScores(_ scores: [Int]) -> [Int] {
    var heap = Heap(scores)
    return (0..<3).compactMap { _ in heap.popMin() }
}
fun topThreeScores(scores: List<Int>): List<Int> {
    val heap = PriorityQueue<Int>()
    heap.addAll(scores)
    return List(3) { heap.poll() }
}
List<Integer> topThreeScores(List<Integer> scores) {
    PriorityQueue<Integer> heap = new PriorityQueue<>();
    heap.addAll(scores);
    return List.of(heap.poll(), heap.poll(), heap.poll());
}
function topThreeScores(scores: number[]): number[] {
  const heap = new Heap<number>((a, b) => a < b);
  for (const s of scores) heap.push(s);
  return [heap.pop()!, heap.pop()!, heap.pop()!];
}

Predict the output

The Kth Largest solution runs on [3, 2, 1, 5, 6, 4] with k = 2. What does the heap contain at the end, and what is returned?

Exercisemedium

Top K Frequent Elements

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

nums = [1, 1, 1, 2, 2, 3], k = 2   → [1, 2]
nums = [1], k = 1                  → [1]
›

Two steps: count how often each number appears (a hash map), then pick the k numbers with the highest counts.

›

For the second step, use the same bounded min-heap as in Kth Largest, ordered by count: keep at most k entries, dropping the least frequent.

SolutionTry it yourself firstTime O(n log k)Space O(n)
func topKFrequent(_ nums: [Int], _ k: Int) -> [Int] {
    let counts = nums.reduce(into: [:]) { $0[$1, default: 0] += 1 }

    struct Item: Comparable {
        let value: Int, count: Int
        static func < (a: Item, b: Item) -> Bool { a.count < b.count }
    }
    var heap = Heap<Item>()
    for (value, count) in counts {
        heap.insert(Item(value: value, count: count))
        if heap.count > k { heap.removeMin() }        // drop the least frequent
    }
    return heap.unordered.map(\.value)
}
fun topKFrequent(nums: IntArray, k: Int): List<Int> {
    val counts = nums.toList().groupingBy { it }.eachCount()
    val heap = PriorityQueue<Map.Entry<Int, Int>>(compareBy { it.value })
    for (entry in counts.entries) {
        heap.add(entry)
        if (heap.size > k) heap.poll()                // drop the least frequent
    }
    return heap.map { it.key }
}
int[] topKFrequent(int[] nums, int k) {
    Map<Integer, Integer> counts = new HashMap<>();
    for (int n : nums) counts.merge(n, 1, Integer::sum);
    PriorityQueue<Map.Entry<Integer, Integer>> heap = new PriorityQueue<>(Map.Entry.comparingByValue());
    for (Map.Entry<Integer, Integer> entry : counts.entrySet()) {
        heap.add(entry);
        if (heap.size() > k) heap.poll();             // drop the least frequent
    }
    return heap.stream().mapToInt(Map.Entry::getKey).toArray();
}
function topKFrequent(nums: number[], k: number): number[] {
  const counts = new Map<number, number>();
  for (const n of nums) counts.set(n, (counts.get(n) ?? 0) + 1);
  const heap = new Heap<[number, number]>((a, b) => a[1] < b[1]);   // [value, count], least frequent on top
  for (const entry of counts) {
    heap.push(entry);
    if (heap.size > k) heap.pop();                  // drop the least frequent
  }
  return heap.unordered.map(([value]) => value);
}

Counting is O(n); the heap step is O(u log k) for u distinct values. Since the problem doesn’t ask for any order, returning the heap’s contents directly is fine. An O(n) alternative: bucket sort by count (an array of lists, index = count, from 1 to n), then read buckets from the highest count down until you have k values. Mention both in an interview and pick by constraints.

A comparator that overflows

Spot the bugBackground jobs run in a strange order

Tap the line with the bug.

struct Job: Comparable {
    let name: String
    let priority: Int                       // Int.min means "whenever"
    static func < (a: Job, b: Job) -> Bool { b.priority - a.priority < 0 }
}
data class Job(val name: String, val priority: Int)   // Int.MIN_VALUE means "whenever"

val jobs = PriorityQueue<Job> { a, b -> b.priority - a.priority }   // highest first
record Job(String name, int priority) {}   // Integer.MIN_VALUE means "whenever"

PriorityQueue<Job> jobs = new PriorityQueue<>((a, b) -> b.priority() - a.priority());   // highest first

  • Top-k uses a bounded heap of the opposite kind: a min-heap keeps the k largest. Top-k frequent: count, then the same heap by count (or bucket sort by count, O(n)).
  • Comparators compare, never subtract (overflow, NaN), and must not depend on fields that change while the element is in the heap.
var top = Heap<Int>()
for x in stream { top.insert(x); if top.count > k { top.removeMin() } }   // keeps the k largest
val top = PriorityQueue<Int>()
for (x in stream) { top.add(x); if (top.size > k) top.poll() }         // keeps the k largest
PriorityQueue<Integer> top = new PriorityQueue<>();
for (int x : stream) { top.add(x); if (top.size() > k) top.poll(); }    // keeps the k largest
const top = new Heap<number>((a, b) => a < b);
for (const x of stream) { top.push(x); if (top.size > k) top.pop(); }    // keeps the k largest

Check this part

  1. Which heap do you use to keep the k largest numbers of a stream?

  2. Why does the Kth Largest solution beat sorting?

  3. A comparator returns b.priority - a.priority < 0, and one priority is Int.min. What happens?A comparator is { a, b -> b.priority - a.priority }, and one priority is Int.MIN_VALUE. What happens?A comparator is (a, b) -> b.priority() - a.priority(), and one priority is Integer.MIN_VALUE. What happens?A comparator is (a, b) => b.priority - a.priority, and "whenever" jobs use -Infinity. What happens with two of them?

  4. Top K Frequent in O(n) instead of O(n log k)?

For the k largest, keep a min-heap of size k and drop its top whenever it grows past k: O(n log k), and it works on streams. Take from the right end, and compare in comparators instead of subtracting.

Part Not started

Merging and medians

You have k sorted lists (log files from k servers, sorted by time) and want one sorted list. Comparing the heads of all k lists at every step costs O(k) per element. A heap makes it O(log k), step by step:

  1. Put the first element of each list into a min-heap, remembering which list it came from and its position.
  2. Pop the smallest: it’s the next element of the merged output.
  3. Push the next element from the same list (if there is one).
  4. Repeat until the heap is empty. The heap never holds more than k entries.
struct Entry: Comparable {
    let value: Int, list: Int, index: Int
    static func < (a: Entry, b: Entry) -> Bool { a.value < b.value }
}

func mergeSorted(_ lists: [[Int]]) -> [Int] {
    var heap = Heap(lists.enumerated().compactMap { i, list in
        list.first.map { Entry(value: $0, list: i, index: 0) }
    })
    var merged: [Int] = []
    while let smallest = heap.popMin() {
        merged.append(smallest.value)
        let next = smallest.index + 1
        if next < lists[smallest.list].count {
            heap.insert(Entry(value: lists[smallest.list][next], list: smallest.list, index: next))
        }
    }
    return merged
}
fun mergeSorted(lists: List<IntArray>): List<Int> {
    // Triple(value, listIndex, elementIndex), ordered by value
    val heap = PriorityQueue<Triple<Int, Int, Int>>(compareBy { it.first })
    lists.forEachIndexed { i, list -> if (list.isNotEmpty()) heap.add(Triple(list[0], i, 0)) }
    val merged = mutableListOf<Int>()
    while (heap.isNotEmpty()) {
        val (value, list, index) = heap.poll()
        merged += value
        if (index + 1 < lists[list].size) heap.add(Triple(lists[list][index + 1], list, index + 1))
    }
    return merged
}
static List<Integer> mergeSorted(List<int[]> lists) {
    // int[] {value, listIndex, elementIndex}, ordered by value
    PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(e -> e[0]));
    for (int i = 0; i < lists.size(); i++) {
        if (lists.get(i).length > 0) heap.add(new int[] {lists.get(i)[0], i, 0});
    }
    List<Integer> merged = new ArrayList<>();
    while (!heap.isEmpty()) {
        int[] e = heap.poll();
        merged.add(e[0]);
        int[] list = lists.get(e[1]);
        if (e[2] + 1 < list.length) heap.add(new int[] {list[e[2] + 1], e[1], e[2] + 1});
    }
    return merged;
}
function mergeSorted(lists: number[][]): number[] {
  // [value, listIndex, elementIndex], ordered by value
  const heap = new Heap<[number, number, number]>((a, b) => a[0] < b[0]);
  lists.forEach((list, i) => { if (list.length > 0) heap.push([list[0], i, 0]); });
  const merged: number[] = [];
  while (heap.size > 0) {
    const [value, list, index] = heap.pop()!;
    merged.push(value);
    if (index + 1 < lists[list].length) heap.push([lists[list][index + 1], list, index + 1]);
  }
  return merged;
}

With n elements in total, that’s O(n log k): merging 100 sorted log files of 10 million lines each is a 7-comparison-per-line job, not a 100-comparison one.

Two heaps: the running median

The median splits values into a lower and an upper half. Keep the lower half in a max-heap and the upper half in a min-heap, balanced so that their sizes differ by at most one. The median is then at the top of one heap or the average of both tops: O(log n) per insert, O(1) per query (the exercise).

Exercisehard

Find Median from Data Stream

Design a structure that supports addNum(num) and findMedian() for a stream of integers. The median of an even count is the average of the two middle values.

addNum(1), addNum(2)  → findMedian() = 1.5
addNum(3)             → findMedian() = 2.0

addNum should be O(log n) and findMedian O(1).

›

The median only depends on the largest value of the lower half and the smallest value of the upper half. Which heaps give you those in O(1)?

›

lower is a max-heap, upper a min-heap. Add to lower, move its max to upper, then if upper is bigger, move its min back. lower always has the same size as upper or one more.

SolutionTry it yourself firstTime O(log n) add, O(1) medianSpace O(n)
struct MedianFinder {
    private var lower = Heap<Int>()                   // use max end: smaller half
    private var upper = Heap<Int>()                   // use min end: larger half

    mutating func addNum(_ num: Int) {
        lower.insert(num)
        upper.insert(lower.removeMax())
        if upper.count > lower.count { lower.insert(upper.removeMin()) }
    }

    func findMedian() -> Double {
        lower.count > upper.count
            ? Double(lower.max!)
            : (Double(lower.max!) + Double(upper.min!)) / 2
    }
}
class MedianFinder {
    private val lower = PriorityQueue<Int>(compareByDescending { it })   // max-heap: smaller half
    private val upper = PriorityQueue<Int>()                             // min-heap: larger half

    fun addNum(num: Int) {
        lower.add(num)
        upper.add(lower.poll())                       // the largest of the lower half moves up
        if (upper.size > lower.size) lower.add(upper.poll())   // keep lower.size >= upper.size
    }

    fun findMedian(): Double =
        if (lower.size > upper.size) lower.peek().toDouble()
        else (lower.peek().toLong() + upper.peek()) / 2.0      // Long: no overflow in the sum
}
class MedianFinder {
    private final PriorityQueue<Integer> lower = new PriorityQueue<>(Comparator.reverseOrder());
    private final PriorityQueue<Integer> upper = new PriorityQueue<>();

    void addNum(int num) {
        lower.add(num);
        upper.add(lower.poll());
        if (upper.size() > lower.size()) lower.add(upper.poll());
    }

    double findMedian() {
        return lower.size() > upper.size()
            ? lower.peek()
            : ((long) lower.peek() + upper.peek()) / 2.0;
    }
}
class MedianFinder {
  private readonly lower = new Heap<number>((a, b) => a > b);   // max-heap: smaller half
  private readonly upper = new Heap<number>((a, b) => a < b);   // min-heap: larger half

  addNum(num: number) {
    this.lower.push(num);
    this.upper.push(this.lower.pop()!);                          // the largest of the lower half moves up
    if (this.upper.size > this.lower.size) this.lower.push(this.upper.pop()!);
  }

  findMedian(): number {
    return this.lower.size > this.upper.size
      ? this.lower.peek()!
      : (this.lower.peek()! + this.upper.peek()!) / 2;
  }
}

Routing every new number through lower and then upper guarantees the halves stay ordered (everything in lower ≤ everything in upper); the final move keeps the sizes balanced. Each add is three heap operations, O(log n). Follow-ups: if all values are in a small range (0–100), keep a count array and find the median in O(range); for a sliding window median, add lazy deletion to both heaps; and in production, p50/p95/p99 latency uses approximate sketches (t-digest, HdrHistogram) instead, because keeping every value isn’t affordable.

  • k-way merge: a heap of (value, list, index) heads, O(n log k) for n elements; databases and log processors do exactly this.
  • Running median: max-heap of the lower half, min-heap of the upper half, sizes within one; sliding-window medians add lazy deletion; production percentiles use sketches (t-digest, HdrHistogram).

Check this part

  1. Merging k sorted lists of n total elements with a heap costs…

  2. After popping the smallest entry, what goes into the heap?

  3. How do two heaps give a running median?

  4. A sliding-window median must remove values that leave the window. Heaps can't remove from the middle cheaply. What do you do?

A heap of the current head of each list merges k sorted lists in O(n log k). Two heaps, a max-heap for the lower half and a min-heap for the upper half, keep a running median in O(log n) per insert.

Key takeaways

  • A heap gives the smallest (or largest) element in O(1) and adds or removes in O(log n).
  • Kotlin and Java PriorityQueue is a min-heap by default; Swift uses Heap from swift-collections; TypeScript needs its own small heap class or a package.
  • For the k largest, keep a min-heap of size k; for the k smallest, a max-heap of size k.
  • A heap of current heads merges k sorted lists in O(n log k).
  • Heap iteration order isn’t sorted; heapify is O(n); arbitrary removal needs lazy deletion.
  • Comparators must compare, not subtract, and must not depend on mutable fields.
  • Two balanced heaps give running medians; approximate sketches replace them at production scale.
  • Heaps power schedulers, timers, routing and k-way merges; recognize “next smallest, repeatedly” as a heap problem.

Finished this page?Nice work, this page is complete!

Mark it as done to track your progress. It's saved in this browser, no account needed.

Its cards are now in your Bites review ⚡
Esc

Type to search the whole site.