Skip to content

Binary heaps

A heap is a complete binary tree stored in an array, with every parent no larger than its children, so the smallest key sits in cell 0 and a priority queue costs O(log n) per operation.

Helps to know firstPriority queues, Storing trees

Helps to know first
This topic

First, the picture

What if a priority queue were a tree kept in an array? Watch each new job climb one path, never the whole array.

A tree stored in an array

Six jobs pushed into a min-heap

bool heap_push(struct heap *h, struct job j){    if (h->size == CAP)        return false;    int i = h->size;    h->items[i] = j;    h->size++;    while (i > 0) {        int parent = (i - 1) / 2;        if (h->items[parent].priority <= h->items[i].priority)            break;        swap_jobs(&h->items[parent], &h->items[i]);        i = parent;    }    return true;} struct job heap_pop(struct heap *h){    struct job top = h->items[0];    h->size--;    h->items[0] = h->items[h->size];    int i = 0;    for (;;) {        int l = 2 * i + 1;        int r = 2 * i + 2;        int smallest = i;        if (l < h->size && h->items[l].priority < h->items[smallest].priority)            smallest = l;        if (r < h->size && h->items[r].priority < h->items[smallest].priority)            smallest = r;        if (smallest == i)            break;        swap_jobs(&h->items[i], &h->items[smallest]);        i = smallest;    }    return top;}    for (int i = 0; i < 6; i++)        heap_push(&h, jobs[i]);    while (h.size > 0) {        struct job j = heap_pop(&h);        printf("%d %s\n", j.priority, j.name);

main has started.

size
0
swaps
0

Output

0.00 / 19.70 s
Describe this picture

Two pictures side by side, with the code under them. On the left, the heap drawn as a tree: cell 0 at the top, the children of cell i below it at cells 2i + 1 and 2i + 2, each node labelled with its cell number. On the right, the first six of the heap’s sixteen cells as a row of boxes, each holding a job’s priority. A ring marks the key on the move, in both pictures; its parent, or its children, are shaded while it is compared with them. A swap carries the two keys past each other along their edge and between their cells. The code pane shows heap_push, heap_pop and main’s loops, with the line about to run marked. The clip pushes the jobs with priorities 5, 3, 8, 1, 9 and 2, then pops once, about 20 seconds. 5 lands in cell 0. 3 lands in cell 1 and swaps with 5. 8 lands in cell 2 and stays. 1 lands in cell 3 and swaps twice, with 5 and then with 3, up to the root. 9 lands in cell 4 and stays. 2 lands in cell 5 and swaps once, with 8. The row then reads 1 3 2 5 9 8. The pop lifts 1 out of the root, moves the last key, 8, into cell 0, and swaps it with 2, the smaller of its children, leaving 2 3 8 5 9; the sixth cell turns grey, outside the heap. Under the pictures are one sentence on the current step and the readouts “size” and “swaps”, which end at 5 and 5. The transport plays and pauses, steps one beat back or forward, and scrubs; a paused step is kept in the page address as heap-array.step.

A tree in an array

A binary heap is a complete binary tree, stored in an array exactly as Tree representations stored one: the children of cell i are cells 2i + 1 and 2i + 2, and its parent is cell (i − 1)/2, rounded down. A heap adds one rule, the heap property: every parent is no larger than its children. This is a min-heap; the smallest key is always at the root, cell 0.

The rule says nothing about two children, or about cousins. After the six pushes the array reads 1 3 2 5 9 8: not sorted. Only each parent is checked against its own children, so 3 sits in cell 1 although 2 is smaller. The second smallest key is in cell 1 or cell 2, never further down.

Two operations keep the rule:

  • Push. Put the new key in the next free cell, the next position in the tree. If it is smaller than its parent, swap them, and repeat from the parent: the key sifts up until its parent is no larger or it reaches the root.
  • Pop. Take the root, the smallest key. Move the last key into the root, then, while it is larger than one of its children, swap it with the smaller child: it sifts down.

Key idea

A push or a pop touches one path from the root to a leaf, never the whole array.

The code and its cost

The heap

The full program, jobs.c, stores jobs, each a priority and a name, in a struct on main’s stack:

struct job {
    int priority;
    const char *name;
};

struct heap {
    struct job items[CAP];
    int size;
};

struct heap h = { .size = 0 };

{ .size = 0 } sets size to 0, and every member it does not name starts at zero too, so the 16 items begin as priority 0 and a null name. A struct job is 16 bytes (4 for the priority, 4 of padding, 8 for the pointer), and struct heap is 264.

Push: sift up

int i = h->size;
h->items[i] = j;
h->size++;
while (i > 0) {
    int parent = (i - 1) / 2;
    if (h->items[parent].priority <= h->items[i].priority)
        break;
    swap_jobs(&h->items[parent], &h->items[i]);
    i = parent;
}

heap_push first returns false if all CAP cells are taken. Then the new job goes in cell size, and the loop walks it up while its parent’s priority is larger. swap_jobs swaps two whole jobs, priority and name together, and counts the swap.

Pop: sift down

struct job top = h->items[0];
h->size--;
h->items[0] = h->items[h->size];
int i = 0;
for (;;) {
    int l = 2 * i + 1;
    int r = 2 * i + 2;
    int smallest = i;
    if (l < h->size && h->items[l].priority < h->items[smallest].priority)
        smallest = l;
    if (r < h->size && h->items[r].priority < h->items[smallest].priority)
        smallest = r;
    if (smallest == i)
        break;
    swap_jobs(&h->items[i], &h->items[smallest]);
    i = smallest;
}
return top;

smallest ends at whichever of the three cells, i and its two children, holds the smallest priority; the checks l < h->size and r < h->size skip children that are past the end. If i is already the smallest, the key is in place. heap_pop reads cell 0 without checking, so it must only be called when size > 0; main’s loop checks that first.

What it costs

A complete tree of n keys has height ⌊log₂ n⌋. A push swaps at most once per level on the way up, and a pop at most once per level on the way down, so each costs O(log n). Looking at the smallest key without removing it is O(1): it is h->items[0]. With more children per node the tree is shallower, which d-ary heaps trade against dearer pops.

Changing a priority

A scheduler sometimes needs to make a waiting job more or less urgent. That is a sift up or a sift down from the job’s cell, but the heap does not know where a job is. The usual answer is a second array, from each job to its current cell, updated inside every swap; then a priority can change in O(log n).

A max-heap

Reverse every comparison (>= for <= in the push, > for < in the pop) and the largest key rises to the root instead: a max-heap.

Running it yourself

gcc -std=c11 -Wall -Wextra -pedantic jobs.c -o jobs
./jobs

It prints heap: 1 3 2 5 9 8, 4 swaps, then the jobs in order of priority: 1 alarm, 2 payroll, 3 email, 5 backup, 8 report, 9 cleanup.

Worked example

Pop by hand from 1 3 2 5 9 8. 1 is the answer. The last key, 8, moves to cell 0. Its children are 3 (cell 1) and 2 (cell 2); 2 is smaller and smaller than 8, so they swap. 8 is now in cell 2, whose children would be cells 5 and 6, past the end of a heap of 5. The array is 2 3 8 5 9, as at the end of the clip.

Push 0 into 1 3 2 5 9 8. 0 lands in cell 6, whose parent is cell 2, holding 2: swap. Now in cell 2, its parent is cell 0, holding 1: swap again. The array is 0 3 1 5 9 8 2, after 2 swaps.

A million jobs. The tree has height ⌊log₂ 1,000,000⌋ = 19, so a push or a pop makes at most 19 swaps.

Two checks. Is 1 2 3 4 5 a min-heap? (Yes: 1 is above 2 and 3, and 2 is above 4 and 5.) Is 1 3 2 2 9? (No: cell 3 holds 2, under cell 1’s 3.) And where is the largest key of a min-heap? (In a leaf: the back half of the array.)

Where it’s used

Priority queues. A heap is the usual way to build one: task schedulers that run the most urgent job next, and event simulations that always take the earliest event.

Shortest paths. Dijkstra’s algorithm (19.5) keeps the places still to visit in a min-heap, ordered by distance.

Merging and top-k. Merging many sorted files keeps one item per file in a heap; keeping the k largest values of a stream keeps them in a min-heap of size k and drops the smallest whenever a larger value arrives.

Sorting. Heapsort builds a heap in the array and pops it into the same array.

Reference card

Code or ideaMeaning
heap propertyevery parent ≤ its children (a min-heap)
cellschildren of i at 2i + 1 and 2i + 2; parent at (i − 1)/2
heap_pushappend, then sift up: O(log n)
heap_poptake cell 0, move the last key there, sift down: O(log n); needs size > 0
peekh->items[0]: O(1)
{ .size = 0 }members not named start at zero
max-heapevery comparison reversed
not sortedonly each parent against its own children

Platform: C11, gcc on x86-64 Linux: int 4 bytes, pointers 8, little-endian. Addresses in the pictures are short stand-ins, by one rule for the whole course; sizes are the real ones.

Keep reading with a free account.

Trees is a members’ section. An account costs nothing.

By creating an account you accept the terms of use.

Sign in

Already have an account? Use the way you signed up.

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

End of the lesson

Where to go next.

New lessons by email

Are you 13 or over?

Accounts and email updates are for people aged 13 or over. We keep only your answer, never your date of birth.

Phasorium

Account

Sign inWith Google, GitHub or your email

Subjects

Site

Look