Heaps & Priority Queues

Keep the smallest or largest item on top with O(log n) inserts and removals: the tool for top-k, running medians and scheduling.

IntermediateRead first: Binary Trees

How It Works

A heap is a binary tree with one ordering rule: every parent is less than or equal to its children. That's all it promises. Siblings come in no particular order, so you can't search a heap the way you search a binary search tree, but the smallest value is always at the root. A priority queue is the interface (add items, take out the smallest) and a binary heap is how nearly every library implements it. Flip the rule and you get a max-heap.

A heap is always a complete tree: every level is full except the last, which fills from the left. That shape packs into a plain array with no pointers. Store the levels one after another, and the children of index i land at 2i + 1 and 2i + 2, with its parent at (i - 1) / 2 rounded down.

          1         index:  0  1  2  3  4  5
        /   \       value: [1, 3, 2, 5, 9, 8]
      3       2
     / \     /      children of i: 2i + 1, 2i + 2
    5   9   8       parent of i:   floor((i - 1) / 2)
The same min-heap as a tree and as the array that stores it. Each parent is <= its children, but the array isn't sorted.

Both operations repair a single root-to-leaf path. push puts the new value in the next free slot and swaps it upward while it's smaller than its parent. pop takes the root, moves the last value into its place, and swaps that one downward until no child is smaller. A complete tree of n nodes is about log n levels tall, so each operation is O(log n). Building a heap from n values at once (heapify) is only O(n), because most nodes sit near the bottom, where they can sink a level or two at most.

Push and Pop on a Min-Heap

Step 1 of 10

as a tree

items

  • Value being sifted
  • Values it's compared with
  • Popped minimum

The demo has pushed 5, 3, 8, 1, 9, leaving [1, 3, 8, 5, 9]. Every parent is <= its children, so the minimum, 1, is at the root.

All 10 steps as text
  1. The demo has pushed 5, 3, 8, 1, 9, leaving [1, 3, 8, 5, 9]. Every parent is <= its children, so the minimum, 1, is at the root.
  2. push(2) puts 2 in the next free slot, index 5, so the tree stays complete. Its parent 8 is bigger, which breaks heap order, so swap them.
  3. 2 moves up to index 2. Its parent 1 is smaller, so heap order holds and push is done.
  4. pop() returns the root, 1, the minimum. To remove it without leaving a hole, first swap it with the last leaf, 8.
  5. 1 comes off the end. 8 is the root now. Its smaller child, 2, is less than 8, so swap with it. That child becomes the parent of the other, so it has to be the smaller one.
  6. 8 sinks to index 2. It has no children, so it's settled. The heap is [2, 3, 8, 5, 9], and pop returns 1.
  7. Pop again. The root 2 is the new minimum, so swap it with the last leaf, 9.
  8. 2 comes off the end. 9 is the root now. Its smaller child, 3, is less than 9, so swap with it.
  9. 9 sinks to index 1. Its only child, 5, is smaller, so swap them.
  10. 9 sinks to index 3. It has no children, so it's settled. The heap is [3, 5, 8, 9], and pop returns 2. Each pop hands back the next smallest value.

Most heap problems take one of three shapes:

  • Top k. Cap the heap at k items and evict whenever it grows past that. To keep the k largest, use a min-heap, so the weakest of them sits on top ready to be evicted.
  • Two heaps. Keep the lower half of the data in a max-heap and the upper half in a min-heap. Their two tops are the middle of the data, which gives you a running median.
  • Always take the cheapest next. Merging k sorted lists, scheduling jobs and Dijkstra's shortest paths all pull the smallest item, then push the new candidates it creates.

When to Reach for It

  • You need the k largest, smallest, closest or most frequent items, and k is much smaller than n.
  • Values arrive one at a time and you keep asking for the current minimum, maximum or median.
  • You repeatedly process the cheapest, earliest or most urgent item, and new items appear as you go.
  • You're merging several sorted lists or streams into one sorted output.
  • A full sort would work, but you only ever look at the front of the order.

Complexity

OperationTimeSpace
Peek at the minimumO(1)O(1)
Push or popO(log n)O(1)
Build from n values (heapify)O(n)O(1) in place
Keep the top k of n valuesO(n log k)O(k)
Find or remove an arbitrary valueO(n)O(1)

The Template

A Binary Min-Heap

The class wraps a plain array and two repair loops. siftUp walks a value toward the root while it's smaller than its parent. siftDown walks a value toward the leaves while a child is smaller, and it always swaps with the smaller child. That child becomes the parent of the other one, so it has to be the smaller of the two.

push 2  [1, 3, 8, 5, 9, 2]  2 < parent 8: swap
        [1, 3, 2, 5, 9, 8]  2 > parent 1: stop

pop     [1, 3, 2, 5, 9, 8]  remove the root, 1
        [8, 3, 2, 5, 9]     last leaf 8 moves up
        [2, 3, 8, 5, 9]     8 > smaller child 2: swap
The last push and the first pop from the demo. Each swap moves one level, so neither can take more swaps than the tree is tall.

The demo pushes six values and prints the array, which is in heap order rather than sorted order. Popping until the heap is empty then returns them smallest first. That's the idea behind heapsort.

You'll rarely write this by hand outside an interview. Every language here except TypeScript ships one:

  • Python: heapq treats a plain list as a min-heap. Use heapq.heappush(h, x), heapq.heappop(h) and h[0] to peek, and heapq.heapify(h) to build one in O(n). Before Python 3.14 there are no max-heap functions, so the usual trick is to push -x and negate on the way out.
  • Java: PriorityQueue<Integer> is a min-heap with offer, poll and peek. Pass Collections.reverseOrder() to the constructor for a max-heap.
  • C#: PriorityQueue<TElement, TPriority> stores each element with a separate priority and dequeues the lowest priority first. Use Enqueue(item, priority), Dequeue() and Peek(), and pass a reversed Comparer for a max-heap.
  • Go: container/heap supplies heap.Push, heap.Pop and heap.Init, but you bring the storage: a type (almost always a slice) with Len, Less, Swap, Push and Pop methods, as in the problems below. The top is h[0].
  • TypeScript: there's no built-in heap. Paste a small class like this one, as the problems below do.

Time O(log n) per push or pop, O(1) to peekSpace O(n)

class MinHeap:
    """A min-heap stored in a flat list. Index 0 is the root, the children of
    index i sit at 2i + 1 and 2i + 2, and its parent at (i - 1) // 2.
    Every parent is <= its children, so the minimum is always at index 0."""

    def __init__(self) -> None:
        self.items: list[int] = []

    def size(self) -> int:
        return len(self.items)

    def peek(self) -> int:
        """The smallest value, without removing it."""
        if not self.items:
            raise IndexError("heap is empty")
        return self.items[0]

    def push(self, value: int) -> None:
        # Add at the end (the next free leaf), then let it rise into place.
        self.items.append(value)
        self._sift_up(len(self.items) - 1)

    def pop(self) -> int:
        """Remove and return the smallest value."""
        top = self.peek()
        # Move the last leaf into the root's slot, then let it sink into place.
        self._swap(0, len(self.items) - 1)
        self.items.pop()
        self._sift_down(0)
        return top

    def _sift_up(self, i: int) -> None:
        while i > 0:
            parent = (i - 1) // 2
            if self.items[parent] <= self.items[i]:
                return  # Heap order holds: done.
            self._swap(i, parent)
            i = parent

    def _sift_down(self, i: int) -> None:
        n = len(self.items)
        while True:
            left, right = 2 * i + 1, 2 * i + 2
            smallest = i
            if left < n and self.items[left] < self.items[smallest]:
                smallest = left
            if right < n and self.items[right] < self.items[smallest]:
                smallest = right
            if smallest == i:
                return  # No child is smaller: done.
            # Swap with the smaller child so the new parent is <= its sibling too.
            self._swap(i, smallest)
            i = smallest

    def _swap(self, a: int, b: int) -> None:
        self.items[a], self.items[b] = self.items[b], self.items[a]


if __name__ == "__main__":
    heap = MinHeap()
    for value in [5, 3, 8, 1, 9, 2]:
        heap.push(value)
    print(heap.items)  # [1, 3, 2, 5, 9, 8]
    print(heap.peek())  # 1
    drained = []
    while heap.size() > 0:
        drained.append(heap.pop())
    print(drained)  # [1, 2, 3, 5, 8, 9]
    print(heap.size())  # 0
// A min-heap stored in a flat array. Index 0 is the root, the children of
// index i sit at 2i + 1 and 2i + 2, and its parent at floor((i - 1) / 2).
// Every parent is <= its children, so the minimum is always at index 0.
export class MinHeap {
  items: number[] = [];

  size(): number {
    return this.items.length;
  }

  // The smallest value, without removing it.
  peek(): number {
    if (this.items.length === 0) throw new Error("heap is empty");
    return this.items[0];
  }

  // Add at the end (the next free leaf), then let it rise into place.
  push(value: number): void {
    this.items.push(value);
    this.siftUp(this.items.length - 1);
  }

  // Remove and return the smallest value.
  pop(): number {
    const top = this.peek();
    // Move the last leaf into the root's slot, then let it sink into place.
    this.swap(0, this.items.length - 1);
    this.items.pop();
    this.siftDown(0);
    return top;
  }

  private siftUp(i: number): void {
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (this.items[parent] <= this.items[i]) return; // Heap order holds: done.
      this.swap(i, parent);
      i = parent;
    }
  }

  private siftDown(i: number): void {
    const n = this.items.length;
    while (true) {
      const left = 2 * i + 1;
      const right = 2 * i + 2;
      let smallest = i;
      if (left < n && this.items[left] < this.items[smallest]) smallest = left;
      if (right < n && this.items[right] < this.items[smallest]) smallest = right;
      if (smallest === i) return; // No child is smaller: done.
      // Swap with the smaller child so the new parent is <= its sibling too.
      this.swap(i, smallest);
      i = smallest;
    }
  }

  private swap(a: number, b: number): void {
    [this.items[a], this.items[b]] = [this.items[b], this.items[a]];
  }
}

const heap = new MinHeap();
for (const value of [5, 3, 8, 1, 9, 2]) heap.push(value);
console.log(JSON.stringify(heap.items)); // [1,3,2,5,9,8]
console.log(heap.peek()); // 1
const drained: number[] = [];
while (heap.size() > 0) drained.push(heap.pop());
console.log(JSON.stringify(drained)); // [1,2,3,5,8,9]
console.log(heap.size()); // 0
package main

import "fmt"

// minHeap is a min-heap stored in a flat slice. Index 0 is the root, the
// children of index i sit at 2i + 1 and 2i + 2, and its parent at (i - 1) / 2.
// Every parent is <= its children, so the minimum is always at index 0.
type minHeap struct {
	items []int
}

func (h *minHeap) size() int { return len(h.items) }

// peek returns the smallest value without removing it.
func (h *minHeap) peek() int {
	if len(h.items) == 0 {
		panic("heap is empty")
	}
	return h.items[0]
}

// push adds at the end (the next free leaf), then lets it rise into place.
func (h *minHeap) push(value int) {
	h.items = append(h.items, value)
	h.siftUp(len(h.items) - 1)
}

// pop removes and returns the smallest value.
func (h *minHeap) pop() int {
	top := h.peek()
	// Move the last leaf into the root's slot, then let it sink into place.
	h.swap(0, len(h.items)-1)
	h.items = h.items[:len(h.items)-1]
	h.siftDown(0)
	return top
}

func (h *minHeap) siftUp(i int) {
	for i > 0 {
		parent := (i - 1) / 2
		if h.items[parent] <= h.items[i] {
			return // Heap order holds: done.
		}
		h.swap(i, parent)
		i = parent
	}
}

func (h *minHeap) siftDown(i int) {
	n := len(h.items)
	for {
		left, right := 2*i+1, 2*i+2
		smallest := i
		if left < n && h.items[left] < h.items[smallest] {
			smallest = left
		}
		if right < n && h.items[right] < h.items[smallest] {
			smallest = right
		}
		if smallest == i {
			return // No child is smaller: done.
		}
		// Swap with the smaller child so the new parent is <= its sibling too.
		h.swap(i, smallest)
		i = smallest
	}
}

func (h *minHeap) swap(a, b int) { h.items[a], h.items[b] = h.items[b], h.items[a] }

func main() {
	heap := &minHeap{}
	for _, value := range []int{5, 3, 8, 1, 9, 2} {
		heap.push(value)
	}
	fmt.Println(heap.items)  // [1 3 2 5 9 8]
	fmt.Println(heap.peek()) // 1
	drained := []int{}
	for heap.size() > 0 {
		drained = append(drained, heap.pop())
	}
	fmt.Println(drained)     // [1 2 3 5 8 9]
	fmt.Println(heap.size()) // 0
}
using System;
using System.Collections.Generic;

public class Solution
{
    // A min-heap stored in a flat list. Index 0 is the root, the children of
    // index i sit at 2i + 1 and 2i + 2, and its parent at (i - 1) / 2.
    // Every parent is <= its children, so the minimum is always at index 0.
    public class MinHeap
    {
        public readonly List<int> Items = new();

        public int Count => Items.Count;

        // The smallest value, without removing it.
        public int Peek()
        {
            if (Items.Count == 0) throw new InvalidOperationException("heap is empty");
            return Items[0];
        }

        // Add at the end (the next free leaf), then let it rise into place.
        public void Push(int value)
        {
            Items.Add(value);
            SiftUp(Items.Count - 1);
        }

        // Remove and return the smallest value.
        public int Pop()
        {
            int top = Peek();
            // Move the last leaf into the root's slot, then let it sink into place.
            Swap(0, Items.Count - 1);
            Items.RemoveAt(Items.Count - 1);
            SiftDown(0);
            return top;
        }

        void SiftUp(int i)
        {
            while (i > 0)
            {
                int parent = (i - 1) / 2;
                if (Items[parent] <= Items[i]) return; // Heap order holds: done.
                Swap(i, parent);
                i = parent;
            }
        }

        void SiftDown(int i)
        {
            int n = Items.Count;
            while (true)
            {
                int left = 2 * i + 1, right = 2 * i + 2;
                int smallest = i;
                if (left < n && Items[left] < Items[smallest]) smallest = left;
                if (right < n && Items[right] < Items[smallest]) smallest = right;
                if (smallest == i) return; // No child is smaller: done.
                // Swap with the smaller child so the new parent is <= its sibling too.
                Swap(i, smallest);
                i = smallest;
            }
        }

        void Swap(int a, int b) => (Items[a], Items[b]) = (Items[b], Items[a]);
    }

    public static void Main()
    {
        var heap = new MinHeap();
        foreach (int value in new[] { 5, 3, 8, 1, 9, 2 }) heap.Push(value);
        Console.WriteLine(Format(heap.Items)); // [1, 3, 2, 5, 9, 8]
        Console.WriteLine(heap.Peek()); // 1
        var drained = new List<int>();
        while (heap.Count > 0) drained.Add(heap.Pop());
        Console.WriteLine(Format(drained)); // [1, 2, 3, 5, 8, 9]
        Console.WriteLine(heap.Count); // 0
    }

    static string Format(List<int> values) => "[" + string.Join(", ", values) + "]";
}
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Solution {
    // A min-heap stored in a flat list. Index 0 is the root, the children of
    // index i sit at 2i + 1 and 2i + 2, and its parent at (i - 1) / 2.
    // Every parent is <= its children, so the minimum is always at index 0.
    static class MinHeap {
        final List<Integer> items = new ArrayList<>();

        int size() {
            return items.size();
        }

        // The smallest value, without removing it.
        int peek() {
            if (items.isEmpty()) throw new IllegalStateException("heap is empty");
            return items.get(0);
        }

        // Add at the end (the next free leaf), then let it rise into place.
        void push(int value) {
            items.add(value);
            siftUp(items.size() - 1);
        }

        // Remove and return the smallest value.
        int pop() {
            int top = peek();
            // Move the last leaf into the root's slot, then let it sink into place.
            swap(0, items.size() - 1);
            items.remove(items.size() - 1);
            siftDown(0);
            return top;
        }

        private void siftUp(int i) {
            while (i > 0) {
                int parent = (i - 1) / 2;
                if (items.get(parent) <= items.get(i)) return; // Heap order holds: done.
                swap(i, parent);
                i = parent;
            }
        }

        private void siftDown(int i) {
            int n = items.size();
            while (true) {
                int left = 2 * i + 1, right = 2 * i + 2;
                int smallest = i;
                if (left < n && items.get(left) < items.get(smallest)) smallest = left;
                if (right < n && items.get(right) < items.get(smallest)) smallest = right;
                if (smallest == i) return; // No child is smaller: done.
                // Swap with the smaller child so the new parent is <= its sibling too.
                swap(i, smallest);
                i = smallest;
            }
        }

        private void swap(int a, int b) {
            Collections.swap(items, a, b);
        }
    }

    public static void main(String[] args) {
        MinHeap heap = new MinHeap();
        for (int value : new int[] {5, 3, 8, 1, 9, 2}) heap.push(value);
        System.out.println(heap.items); // [1, 3, 2, 5, 9, 8]
        System.out.println(heap.peek()); // 1
        List<Integer> drained = new ArrayList<>();
        while (heap.size() > 0) drained.add(heap.pop());
        System.out.println(drained); // [1, 2, 3, 5, 8, 9]
        System.out.println(heap.size()); // 0
    }
}

Worked Problems

Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.

Kth Largest Element in an Array

MEDIUMLeetCode 215

Problem

Given an integer array nums and an integer k, return the kth largest value: the one that would sit at position k if you sorted nums from largest to smallest. Duplicates count separately, so the 2nd largest value in [3, 3, 1] is 3.

Example: nums = [3, 2, 1, 5, 6, 4] with k = 2 returns 5. From largest to smallest the values are 6, 5, 4, 3, 2, 1.

Show Solution

Approach

Sorting answers this in O(n log n), but it puts all n values in order when you only care about k of them. Instead, keep a min-heap of the k largest values seen so far. Push each number, and whenever the heap grows past k, pop its smallest. After the last number the heap holds exactly the top k, and its root (the smallest of them) is the answer.

Why a min-heap to find large values? A new number only belongs in the top k if it beats the weakest member, and that weakest member is also the one to evict. A min-heap keeps exactly that value on top. A max-heap would keep the best value on top, which this problem never needs to look at.

Python, Go, C# and Java use their built-in heaps. The TypeScript version carries a trimmed copy of the template's heap.

Time O(n log k)Space O(k)

import heapq


def find_kth_largest(nums: list[int], k: int) -> int:
    """The kth largest value in `nums`, counting duplicates."""
    heap: list[int] = []  # The k largest values seen so far.
    for num in nums:
        heapq.heappush(heap, num)
        # Evict the smallest: k values at least as big sit above it, so it's out of the top k.
        if len(heap) > k:
            heapq.heappop(heap)
    # The weakest member of the top k is the kth largest.
    return heap[0]


if __name__ == "__main__":
    print(find_kth_largest([3, 2, 1, 5, 6, 4], 2))  # 5
    print(find_kth_largest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4))  # 4
    print(find_kth_largest([-1, 2, 0], 3))  # -1
// The kth largest value in `nums`, counting duplicates.
export function findKthLargest(nums: number[], k: number): number {
  const heap = new MinHeap(); // The k largest values seen so far.
  for (const num of nums) {
    heap.push(num);
    // Evict the smallest: k values at least as big sit above it, so it's out of the top k.
    if (heap.size() > k) heap.pop();
  }
  // The weakest member of the top k is the kth largest.
  return heap.peek();
}

// TypeScript has no built-in heap, so this is the template's min-heap, trimmed.
class MinHeap {
  private items: number[] = [];

  size(): number {
    return this.items.length;
  }

  peek(): number {
    return this.items[0];
  }

  push(value: number): void {
    const items = this.items;
    items.push(value);
    let i = items.length - 1;
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (items[parent] <= items[i]) break;
      [items[i], items[parent]] = [items[parent], items[i]];
      i = parent;
    }
  }

  pop(): number {
    const items = this.items;
    const top = items[0];
    [items[0], items[items.length - 1]] = [items[items.length - 1], items[0]];
    items.pop();
    let i = 0;
    while (true) {
      let smallest = i;
      for (const child of [2 * i + 1, 2 * i + 2]) {
        if (child < items.length && items[child] < items[smallest]) smallest = child;
      }
      if (smallest === i) break;
      [items[i], items[smallest]] = [items[smallest], items[i]];
      i = smallest;
    }
    return top;
  }
}

console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2)); // 5
console.log(findKthLargest([3, 2, 3, 1, 2, 4, 5, 5, 6], 4)); // 4
console.log(findKthLargest([-1, 2, 0], 3)); // -1
package main

import (
	"container/heap"
	"fmt"
)

// intHeap is a min-heap of ints. container/heap does the sifting; you supply
// the storage through these five methods.
type intHeap []int

func (h intHeap) Len() int           { return len(h) }
func (h intHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h intHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *intHeap) Push(x any)        { *h = append(*h, x.(int)) }
func (h *intHeap) Pop() any {
	old := *h
	last := old[len(old)-1]
	*h = old[:len(old)-1]
	return last
}

// findKthLargest returns the kth largest value in nums, counting duplicates.
func findKthLargest(nums []int, k int) int {
	h := &intHeap{} // The k largest values seen so far.
	for _, num := range nums {
		heap.Push(h, num)
		// Evict the smallest: k values at least as big sit above it, so it's out of the top k.
		if h.Len() > k {
			heap.Pop(h)
		}
	}
	// The weakest member of the top k is the kth largest.
	return (*h)[0]
}

func main() {
	fmt.Println(findKthLargest([]int{3, 2, 1, 5, 6, 4}, 2))          // 5
	fmt.Println(findKthLargest([]int{3, 2, 3, 1, 2, 4, 5, 5, 6}, 4)) // 4
	fmt.Println(findKthLargest([]int{-1, 2, 0}, 3))                  // -1
}
using System;
using System.Collections.Generic;

public class Solution
{
    // The kth largest value in nums, counting duplicates.
    public static int FindKthLargest(int[] nums, int k)
    {
        var heap = new PriorityQueue<int, int>(); // The k largest values seen so far.
        foreach (int num in nums)
        {
            heap.Enqueue(num, num); // Each value is its own priority.
            // Evict the smallest: k values at least as big sit above it, so it's out of the top k.
            if (heap.Count > k) heap.Dequeue();
        }
        // The weakest member of the top k is the kth largest.
        return heap.Peek();
    }

    public static void Main()
    {
        Console.WriteLine(FindKthLargest(new[] { 3, 2, 1, 5, 6, 4 }, 2)); // 5
        Console.WriteLine(FindKthLargest(new[] { 3, 2, 3, 1, 2, 4, 5, 5, 6 }, 4)); // 4
        Console.WriteLine(FindKthLargest(new[] { -1, 2, 0 }, 3)); // -1
    }
}
import java.util.PriorityQueue;

public class Solution {
    // The kth largest value in nums, counting duplicates.
    static int findKthLargest(int[] nums, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>(); // The k largest values seen so far.
        for (int num : nums) {
            heap.offer(num);
            // Evict the smallest: k values at least as big sit above it, so it's out of the top k.
            if (heap.size() > k) heap.poll();
        }
        // The weakest member of the top k is the kth largest.
        return heap.peek();
    }

    public static void main(String[] args) {
        System.out.println(findKthLargest(new int[] {3, 2, 1, 5, 6, 4}, 2)); // 5
        System.out.println(findKthLargest(new int[] {3, 2, 3, 1, 2, 4, 5, 5, 6}, 4)); // 4
        System.out.println(findKthLargest(new int[] {-1, 2, 0}, 3)); // -1
    }
}

Find Median from Data Stream

HARDLeetCode 295

Problem

Build a class with two methods. addNum(num) takes the next integer from a stream, and findMedian() returns the median of every number added so far. With an odd count, the median is the middle value. With an even count, it's the mean of the two middle values.

Example: after adding 1 and 2, findMedian() returns 1.5. After also adding 3, it returns 2.0.

Show Solution

Approach

Keeping a sorted list costs O(n) per insert, and that adds up over a long stream. But the median only depends on the one or two values in the middle. So keep the smaller half of the numbers in a max-heap low and the larger half in a min-heap high. Their tops are the middle values.

Two rules make that true: every value in low is <= every value in high, and low holds as many values as high or one more. addNum restores both in three steps:

  1. Push num into low.
  2. Pop the largest value from low and push it into high. That value is at least as big as everything left in low, and the rest of low was already below high, so the halves stay in order whichever side num belonged on.
  3. If high now holds more values than low, move its smallest back into low.
add  low (max-heap)  high (min-heap)  median
5    [5]             []               5.0
15   [5]             [15]             10.0
1    [1, 5]          [15]             5.0
3    [1, 3]          [5, 15]          4.0
Heap contents listed in sorted order. The median only ever reads the largest value in low and the smallest in high.

findMedian reads the tops: the top of low alone when the count is odd, or the mean of both tops when it's even. In Java, C# and Go, divide by 2.0 or convert to floating point first, because integer division would turn 1.5 into 1.

Time O(log n) per addNum, O(1) per findMedianSpace O(n)

import heapq


class MedianFinder:
    """Running median of a stream, kept in two heaps that split it in half."""

    def __init__(self) -> None:
        # Before Python 3.14, heapq only makes min-heaps, so low stores negated values.
        self.low: list[int] = []  # Max-heap of the smaller half.
        self.high: list[int] = []  # Min-heap of the larger half.

    def add_num(self, num: int) -> None:
        # Pass every number through low, so the biggest of the smaller half moves
        # up to high. That keeps everything in low <= everything in high.
        heapq.heappush(self.low, -num)
        heapq.heappush(self.high, -heapq.heappop(self.low))
        # Rebalance so low holds as many values as high, or one more.
        if len(self.high) > len(self.low):
            heapq.heappush(self.low, -heapq.heappop(self.high))

    def find_median(self) -> float:
        # Odd count: the middle value is the top of low.
        if len(self.low) > len(self.high):
            return float(-self.low[0])
        # Even count: average the two middle values.
        return (-self.low[0] + self.high[0]) / 2


if __name__ == "__main__":
    # Prints 1.0 1.5 2.0, then 5.0 10.0 5.0 4.0 5.0, then -1.0 -1.5, one per line.
    for stream in [[1, 2, 3], [5, 15, 1, 3, 8], [-1, -2]]:
        finder = MedianFinder()
        for num in stream:
            finder.add_num(num)
            print(f"{finder.find_median():.1f}")
// Running median of a stream, kept in two heaps that split it in half.
export class MedianFinder {
  private low = new Heap((a, b) => a > b); // Max-heap of the smaller half.
  private high = new Heap((a, b) => a < b); // Min-heap of the larger half.

  addNum(num: number): void {
    // Pass every number through low, so the biggest of the smaller half moves
    // up to high. That keeps everything in low <= everything in high.
    this.low.push(num);
    this.high.push(this.low.pop());
    // Rebalance so low holds as many values as high, or one more.
    if (this.high.size() > this.low.size()) this.low.push(this.high.pop());
  }

  findMedian(): number {
    // Odd count: the middle value is the top of low.
    if (this.low.size() > this.high.size()) return this.low.peek();
    // Even count: average the two middle values.
    return (this.low.peek() + this.high.peek()) / 2;
  }
}

// TypeScript has no built-in heap. This one keeps the value for which
// before(value, other) holds at the top, so it works as a min- or max-heap.
class Heap {
  private items: number[] = [];
  private readonly before: (a: number, b: number) => boolean;

  constructor(before: (a: number, b: number) => boolean) {
    this.before = before;
  }

  size(): number {
    return this.items.length;
  }

  peek(): number {
    return this.items[0];
  }

  push(value: number): void {
    const items = this.items;
    items.push(value);
    let i = items.length - 1;
    while (i > 0) {
      const parent = Math.floor((i - 1) / 2);
      if (!this.before(items[i], items[parent])) break;
      [items[i], items[parent]] = [items[parent], items[i]];
      i = parent;
    }
  }

  pop(): number {
    const items = this.items;
    const top = items[0];
    [items[0], items[items.length - 1]] = [items[items.length - 1], items[0]];
    items.pop();
    let i = 0;
    while (true) {
      let first = i;
      for (const child of [2 * i + 1, 2 * i + 2]) {
        if (child < items.length && this.before(items[child], items[first])) first = child;
      }
      if (first === i) break;
      [items[i], items[first]] = [items[first], items[i]];
      i = first;
    }
    return top;
  }
}

// Prints 1.0 1.5 2.0, then 5.0 10.0 5.0 4.0 5.0, then -1.0 -1.5, one per line.
for (const stream of [[1, 2, 3], [5, 15, 1, 3, 8], [-1, -2]]) {
  const finder = new MedianFinder();
  for (const num of stream) {
    finder.addNum(num);
    console.log(finder.findMedian().toFixed(1));
  }
}
package main

import (
	"container/heap"
	"fmt"
)

// intHeap is a min-heap of ints for container/heap.
type intHeap []int

func (h intHeap) Len() int           { return len(h) }
func (h intHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h intHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *intHeap) Push(x any)        { *h = append(*h, x.(int)) }
func (h *intHeap) Pop() any {
	old := *h
	last := old[len(old)-1]
	*h = old[:len(old)-1]
	return last
}

// medianFinder keeps the running median of a stream in two heaps that split
// it in half. intHeap is a min-heap, so low stores negated values.
type medianFinder struct {
	low  intHeap // Max-heap of the smaller half.
	high intHeap // Min-heap of the larger half.
}

func (m *medianFinder) addNum(num int) {
	// Pass every number through low, so the biggest of the smaller half moves
	// up to high. That keeps everything in low <= everything in high.
	heap.Push(&m.low, -num)
	heap.Push(&m.high, -heap.Pop(&m.low).(int))
	// Rebalance so low holds as many values as high, or one more.
	if m.high.Len() > m.low.Len() {
		heap.Push(&m.low, -heap.Pop(&m.high).(int))
	}
}

func (m *medianFinder) findMedian() float64 {
	// Odd count: the middle value is the top of low.
	if m.low.Len() > m.high.Len() {
		return float64(-m.low[0])
	}
	// Even count: average the two middle values. Convert before dividing so
	// integer division doesn't drop the .5.
	return float64(-m.low[0]+m.high[0]) / 2
}

func main() {
	// Prints 1.0 1.5 2.0, then 5.0 10.0 5.0 4.0 5.0, then -1.0 -1.5, one per line.
	for _, stream := range [][]int{{1, 2, 3}, {5, 15, 1, 3, 8}, {-1, -2}} {
		finder := &medianFinder{}
		for _, num := range stream {
			finder.addNum(num)
			fmt.Printf("%.1f\n", finder.findMedian())
		}
	}
}
using System;
using System.Collections.Generic;
using System.Globalization;

public class Solution
{
    // Running median of a stream, kept in two heaps that split it in half.
    public class MedianFinder
    {
        // Max-heap of the smaller half: a reversed comparer puts the largest on top.
        readonly PriorityQueue<int, int> low = new(Comparer<int>.Create((a, b) => b.CompareTo(a)));
        readonly PriorityQueue<int, int> high = new(); // Min-heap of the larger half.

        public void AddNum(int num)
        {
            // Pass every number through low, so the biggest of the smaller half moves
            // up to high. That keeps everything in low <= everything in high.
            low.Enqueue(num, num);
            int moved = low.Dequeue();
            high.Enqueue(moved, moved);
            // Rebalance so low holds as many values as high, or one more.
            if (high.Count > low.Count)
            {
                int back = high.Dequeue();
                low.Enqueue(back, back);
            }
        }

        public double FindMedian()
        {
            // Odd count: the middle value is the top of low.
            if (low.Count > high.Count) return low.Peek();
            // Even count: average the two middle values. Dividing by 2.0, not 2,
            // keeps the .5 that integer division would drop.
            return (low.Peek() + high.Peek()) / 2.0;
        }
    }

    public static void Main()
    {
        // Prints 1.0 1.5 2.0, then 5.0 10.0 5.0 4.0 5.0, then -1.0 -1.5, one per line.
        foreach (int[] stream in new[] { new[] { 1, 2, 3 }, new[] { 5, 15, 1, 3, 8 }, new[] { -1, -2 } })
        {
            var finder = new MedianFinder();
            foreach (int num in stream)
            {
                finder.AddNum(num);
                Console.WriteLine(finder.FindMedian().ToString("F1", CultureInfo.InvariantCulture));
            }
        }
    }
}
import java.util.Collections;
import java.util.Locale;
import java.util.PriorityQueue;

public class Solution {
    // Running median of a stream, kept in two heaps that split it in half.
    static class MedianFinder {
        // Max-heap of the smaller half: a reversed comparator puts the largest on top.
        private final PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder());
        private final PriorityQueue<Integer> high = new PriorityQueue<>(); // Min-heap of the larger half.

        void addNum(int num) {
            // Pass every number through low, so the biggest of the smaller half moves
            // up to high. That keeps everything in low <= everything in high.
            low.offer(num);
            high.offer(low.poll());
            // Rebalance so low holds as many values as high, or one more.
            if (high.size() > low.size()) low.offer(high.poll());
        }

        double findMedian() {
            // Odd count: the middle value is the top of low.
            if (low.size() > high.size()) return low.peek();
            // Even count: average the two middle values. Dividing by 2.0, not 2,
            // keeps the .5 that integer division would drop.
            return (low.peek() + high.peek()) / 2.0;
        }
    }

    public static void main(String[] args) {
        // Prints 1.0 1.5 2.0, then 5.0 10.0 5.0 4.0 5.0, then -1.0 -1.5, one per line.
        for (int[] stream : new int[][] {{1, 2, 3}, {5, 15, 1, 3, 8}, {-1, -2}}) {
            MedianFinder finder = new MedianFinder();
            for (int num : stream) {
                finder.addNum(num);
                System.out.println(String.format(Locale.ROOT, "%.1f", finder.findMedian()));
            }
        }
    }
}

Common Pitfalls

  • Using a max-heap to find the k largest values. Keep a min-heap of size k instead: its root is the weakest of the current top k, which is exactly the value a newcomer has to beat.
  • Forgetting which way the built-in points. Python's heapq, Java's PriorityQueue and C#'s PriorityQueue are all min-heaps. For a max-heap, negate the values or pass a reversed comparator.
  • Treating the heap's array as sorted. Only the root is guaranteed. To read values in order, pop them one at a time.
  • Writing a Java max-heap comparator as (a, b) -> b - a. The subtraction overflows for values near the int limits and silently breaks the order. Use Collections.reverseOrder() or Integer.compare(b, a).
  • Trying to change or remove an item in the middle of a heap. Built-in heaps need O(n) just to find it. Push the updated entry instead, and skip stale entries when they reach the top (lazy deletion).

Practice

More problems to try on your own.