◐ ai-teacher

🧩 Data Structures & Algorithms · Chapter 14

Heaps and priority queues

A heap never fully sorts anything. It just keeps the smallest item on top — and that lazy promise is exactly what schedulers, maps and top-10 lists need.

Study this lesson free in the app →

The basics

A heap is a binary tree stored in an array where every parent is smaller than or equal to its children (a min-heap; a max-heap flips it). A priority queue is the job it does: add items with a priority, and always remove the most urgent one next.

The problem it solves: you keep needing "the smallest so far" while new items keep arriving. A sorted list gives the minimum instantly but costs O(n) per insert; an unsorted list inserts in O(1) but costs O(n) to find the minimum. A heap does both in O(log n), and peeking at the minimum is O(1).

The mental model: a hospital emergency room. Patients don't queue by arrival; the most urgent is always seen next. Nobody sorts the whole waiting room — the staff just make sure the most urgent person is at the front.

Words you must know 5

Heap property
parent ≤ children (min-heap). Says nothing about left vs right
Complete tree
every level full except the last, filled left to right, so it fits in an array with no gaps
Array indexing
children of i are 2i+1 and 2i+2; parent is (i−1)//2
Sift up / sift down
swap a node with its parent / smaller child until order is restored
Heapify
turn an unordered array into a heap in O(n)

Explained

Your phone's map app finds the fastest route by repeatedly asking: "Of all the places I can reach, which is closest right now?" — millions of times.

Ask that of an unsorted list and each answer costs O(n). Ask a heap and it costs O(log n): for a million candidates, about 20 steps instead of a million.

The emergency room

Patients arrive in any order: a sprained ankle, a broken arm, a heart attack, a cough.

The nurse doesn't sort the whole waiting room every time someone walks in. She keeps one promise: the most urgent patient is always at the front. When a new patient arrives, she compares them with whoever they'd displace and moves them up only as far as needed. When the doctor takes the front patient, the next most urgent steps forward after a few quick comparisons.

The ankle and the cough might sit in any order behind the front — nobody cares until they're next. That partial order is a heap. Full sorting would be wasted work.

A tree that lives in an array

index:   0  1  2  3  4  5
array: [ 1, 3, 2, 5, 9, 8 ]

            1          ← index 0: always the minimum
          /   \
         3     2
        / \   /
       5   9 8

Node i has children 2i+1 and 2i+2, parent (i−1)//2. The tree is complete, so there are no gaps and height is ⌊log₂ n⌋.

Push (O(log n)): append at the end, then sift up — swap with the parent while smaller. Pop (O(log n)): take index 0, move the last element to the root, then sift down — swap with the smaller child while bigger than it. Peek: heap[0], O(1).

Only parent–child order is guaranteed. 3 and 2 are siblings in either order; 9 is a leaf even though it's larger than 8's parent.

Six inserts, two extracts

Watch 1 climb from the bottom to the root in two swaps, and 2 climb one level. Then extract removes 1: the last item (8) jumps to the root and sinks below its smaller child. The array ends as [3, 5, 8, 9] — a valid heap, but not sorted order in general.

A priority queue at work

Think of the numbers as urgency levels (1 = most urgent). Extracts return 4, then 2, then 5 — always the most urgent item present at that moment, even though 2 arrived after 4 was served. That's the difference from a plain queue, which would serve strictly by arrival.

heapq in practice

import heapq

h = [5, 3, 8, 1, 9, 2]
heapq.heapify(h)          # O(n) → [1, 3, 2, 5, 9, 8]
heapq.heappush(h, 4)      # O(log n)
heapq.heappop(h)          # 1, O(log n)
h[0]                      # 2 — peek, O(1)

# Max-heap: negate
mx = []
for x in [5, 3, 8]:
    heapq.heappush(mx, -x)
-mx[0]                    # 8

# Priority queue of tasks; the counter breaks ties so
# two tasks are never compared directly
tasks, n = [], 0
for prio, name in [(2, "email"), (1, "outage"), (2, "report")]:
    heapq.heappush(tasks, (prio, n, name)); n += 1
heapq.heappop(tasks)[2]   # 'outage'

# Top-k largest in O(n log k), O(k) memory
def top_k(nums, k):
    h = []
    for x in nums:
        heapq.heappush(h, x)
        if len(h) > k:
            heapq.heappop(h)  # evict the smallest of the k+1
    return sorted(h, reverse=True)

top_k([4, 1, 9, 7, 3, 8], 3)  # [9, 8, 7]

What it costs, and where it's used

operation            binary heap   sorted array   unsorted array
─────────────────────────────────────────────────────────────
peek min             O(1)          O(1)           O(n)
insert               O(log n)      O(n)           O(1)
extract min          O(log n)      O(1)*          O(n)
build from n items   O(n)          O(n log n)     O(1)
search any value     O(n)          O(log n)       O(n)

\* if the minimum is kept at the end.

Why heapify is O(n): half the nodes are leaves and never move; a quarter move at most 1 level; an eighth at most 2… The sum n/4·1 + n/8·2 + n/16·3 + … stays below n.

Where heaps run: Dijkstra's shortest paths and A* (map routing), OS and job schedulers, event simulation, merging k sorted files (O(N log k)), top-k dashboards, the running median of a stream (two heaps).

Gotchas

Words to know

Binary heapA complete binary tree stored in an array where each parent is ≤ (min-heap) or ≥ (max-heap) its children.
Priority queueAn abstract structure that always removes the highest-priority item next; usually implemented with a heap.
Sift upAfter inserting at the end, swap the item with its parent until the heap property holds.
Sift downAfter moving an item to the root, swap it with its smaller child until the heap property holds.
HeapifyBuilding a heap from an unordered array in O(n) by sifting down from the last parent to the root.
Complete binary treeEvery level is full except possibly the last, which is filled from the left — so it fits in an array without gaps.
Heap sortHeapify, then repeatedly extract the root: O(n log n) time, O(1) extra space, not stable.

In two minutes

Test yourself

1. Which property defines a min-heap?

  1. Every left child is smaller than its right sibling
  2. The underlying array is sorted ascending
  3. Every parent is less than or equal to its children
  4. Left subtree < node < right subtree
Show the answer

Every parent is less than or equal to its children — Only parent–child order is guaranteed; siblings can be in any order and the array is generally not sorted. 'Left < node < right' is the BST property — a different structure.

2. Costs of peek, insert and extract-min on a binary heap of n items?

  1. O(1), O(log n), O(log n)
  2. O(1), O(1), O(1)
  3. O(1), O(log n), O(n)
  4. O(log n), O(n), O(log n)
Show the answer

O(1), O(log n), O(log n) — The minimum sits at index 0. Insert and extract each move one item along a single root-to-leaf path, whose length is the height ⌊log₂ n⌋.

3. In a 0-indexed array heap, where are the children of the node at index i?

  1. i + 1 and i + 2
  2. 2i and 2i + 1
  3. i // 2 and i // 2 + 1
  4. 2i + 1 and 2i + 2
Show the answer

2i + 1 and 2i + 2 — Root 0 has children 1 and 2; node 1 has 3 and 4; node 2 has 5 and 6. 2i and 2i+1 is the 1-indexed version, which breaks at the root (0 would be its own child).

4. A game streams 10 million scores; you need the top 10 at the end with little memory. Best approach?

  1. Store all, sort descending, take 10
  2. Keep a min-heap of size 10; when a score beats the root, replace the root
  3. Push all 10 million into a max-heap, then pop 10 times
  4. Insert every score into a balanced BST
Show the answer

Keep a min-heap of size 10; when a score beats the root, replace the root — The size-10 min-heap holds the current top 10 with the weakest at the root, ready to be evicted: O(n log 10) time, O(10) memory. The other options store all 10 million items (O(n) memory), and sorting costs O(n log n).

5. How do you get a max-heap from Python's heapq?

  1. heapq.maxheap(list)
  2. Push negated priorities (or tuples like (-priority, item)) and negate when popping
  3. heapq.heappush(h, x, reverse=True)
  4. It's impossible in Python
Show the answer

Push negated priorities (or tuples like (-priority, item)) and negate when popping — heapq only implements a min-heap on a plain list. Negating flips the order: the largest original value becomes the smallest stored one. There is no reverse argument and no maxheap function in the public API.

6. A job scheduler must always run the highest-priority job next while new jobs keep arriving. Which structure?

  1. A sorted list, inserting with bisect
  2. A stack
  3. A hash map from priority to job
  4. A priority queue backed by a binary heap
Show the answer

A priority queue backed by a binary heap — A heap gives O(log n) insert and O(log n) removal of the top. bisect finds the position in O(log n) but inserting into a Python list shifts elements: O(n). A stack serves the newest job, and a hash map has no notion of 'smallest key'.

There are 8 questions, 4 puzzles and 6 recall cards for this lesson in the app — the recall cards come back on later days, just before you would forget.

Learn it so it stays. Read it, test yourself, then recall it on two different days. The app schedules that for you, free.

Start this lesson →