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
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 idea | Meaning |
|---|---|
| heap property | every parent ≤ its children (a min-heap) |
| cells | children of i at 2i + 1 and 2i + 2; parent at (i − 1)/2 |
heap_push | append, then sift up: O(log n) |
heap_pop | take cell 0, move the last key there, sift down: O(log n); needs size > 0 |
| peek | h->items[0]: O(1) |
{ .size = 0 } | members not named start at zero |
| max-heap | every comparison reversed |
| not sorted | only 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.