Specialized heaps
Heaps beyond the binary heap: d-ary heaps, binomial heaps, Fibonacci heaps, pairing heaps, the indexed priority queue and decrease-key.
Learning goals
- Tune a d-ary heap's branching factor to the workload
- Describe how binomial, Fibonacci and pairing heaps merge in less than O(n)
- Implement an indexed priority queue with decrease-key
- Compare the operation costs of the heap family
Prerequisites
The binary heap is the default, but algorithms like Dijkstra and Prim call decrease-key far more often than they extract, and mergeable heaps are needed for some parallel and incremental algorithms. Other heaps trade simplicity for those operations.
D-ary heap
A **d-ary heap** gives each node up to d children: the parent of i is (i - 1) / d and the children start at d · i + 1. The tree is shallower (height log base d of n), so **sift-up** is cheaper, but **sift-down** must compare up to d children per level, so it is dearer. Workloads with many insertions or decrease-keys favour a larger d; workloads with many extractions favour d = 2. A 4-ary heap is also friendlier to the cache, since the children are adjacent in the array.
The animation inserts and then extracts the 14 values 14 down to 1: d = 2 uses 31 comparisons for the insertions and 42 for the extractions (73 in all), d = 3 uses 24 and 45 (69), and d = 4 uses 22 and 54 (76). The ascending-input preset shows the cheapest case for insertion: each of the 12 values needs one comparison, 11 in all, since the first insertion needs none.
| Operation | Binary heap (d = 2) | d-ary heap |
|---|---|---|
| Height | log2 n | log_d n |
| insert / decrease-key | O(log2 n) | O(log_d n) |
| extract-min | O(log2 n) | O(d · log_d n) |
1static class DaryHeap {2private final int d;3private int[] a = new int[16];4private int size;5DaryHeap(int d) { this.d = d; }6void add(int x) {7if (size == a.length) a = Arrays.copyOf(a, 2 * size);8a[size] = x;9int i = size++;10while (i > 0 && a[(i - 1) / d] > a[i]) { // sift-up: one comparison per level11int p = (i - 1) / d;12int t = a[p]; a[p] = a[i]; a[i] = t;13i = p;14}15}16int poll() {17int min = a[0];18a[0] = a[--size];19int i = 0;20while (true) { // sift-down: up to d comparisons per level21int m = i;22for (int c = d * i + 1; c < Math.min(size, d * i + 1 + d); c++) if (a[c] < a[m]) m = c;23if (m == i) break;24int t = a[i]; a[i] = a[m]; a[m] = t;25i = m;26}27return min;28}29boolean isEmpty() { return size == 0; }30}
d-ary heap: how the branching factor changes sift-up and sift-down cost
Interactive · edit the inputheap array (parent of i is ⌊(i - 1) / 4⌋)
A 4-ary heap gives each node up to 4 children, so the tree is shallower (height ⌊log4 n⌋) but a sift-down must compare 4 children per level. Insert all values, then extract them all.
1static int d = 4; // branching factor2static int parent(int i) { return (i - 1) / d; }3static int firstChild(int i) { return d * i + 1; }4static void siftUp(int[] a, int i) {5while (i > 0 && a[parent(i)] > a[i]) { // one comparison per level6swap(a, parent(i), i);7i = parent(i);8}9}10static void siftDown(int[] a, int n, int i) {11while (true) {12int m = i;13for (int c = firstChild(i); c < Math.min(n, firstChild(i) + d); c++) // up to d comparisons per level14if (a[c] < a[m]) m = c;15if (m == i) return;16swap(a, i, m);17i = m;18}19}
Live state
d
4
size
0
Execution trace (dry run table)Show 25 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | d=4 size=0 | A 4-ary heap gives each node up to 4 children, so the tree is shallower (height ⌊log4 n⌋) but a sift-down must compare 4 children per level. Insert all values, then extract them all. |
| 2 | L4 | d=4 size=1 | Insert 9: it moved up 0 levels. Sift-up costs one comparison per level. |
| 3 | L4 | d=4 size=2 | Insert 7: it moved up 1 level. Sift-up costs one comparison per level. |
| 4 | L4 | d=4 size=3 | Insert 8: it moved up 0 levels. Sift-up costs one comparison per level. |
| 5 | L4 | d=4 size=4 | Insert 3: it moved up 1 level. Sift-up costs one comparison per level. |
| 6 | L4 | d=4 size=5 | Insert 5: it moved up 0 levels. Sift-up costs one comparison per level. |
| 7 | L4 | d=4 size=6 | Insert 2: it moved up 2 levels. Sift-up costs one comparison per level. |
| 8 | L4 | d=4 size=7 | Insert 6: it moved up 0 levels. Sift-up costs one comparison per level. |
| 9 | L4 | d=4 size=8 | Insert 1: it moved up 2 levels. Sift-up costs one comparison per level. |
| 10 | L4 | d=4 size=9 | Insert 4: it moved up 0 levels. Sift-up costs one comparison per level. |
| 11 | L4 | d=4 size=10 | Insert 10: it moved up 0 levels. Sift-up costs one comparison per level. |
| 12 | L4 | d=4 size=11 | Insert 12: it moved up 0 levels. Sift-up costs one comparison per level. |
| 13 | L4 | d=4 size=12 | Insert 11: it moved up 0 levels. Sift-up costs one comparison per level. |
| 14 | L10 | d=4 size=11 | Extract 1: the last leaf 11 went to the root and sank 2 levels. Each level costs up to 4 comparisons. |
| 15 | L10 | d=4 size=10 | Extract 2: the last leaf 12 went to the root and sank 2 levels. Each level costs up to 4 comparisons. |
| 16 | L10 | d=4 size=9 | Extract 3: the last leaf 10 went to the root and sank 2 levels. Each level costs up to 4 comparisons. |
| 17 | L10 | d=4 size=8 | Extract 4: the last leaf 12 went to the root and sank 1 level. Each level costs up to 4 comparisons. |
| 18 | L10 | d=4 size=7 | Extract 5: the last leaf 11 went to the root and sank 2 levels. Each level costs up to 4 comparisons. |
| 19 | L10 | d=4 size=6 | Extract 6: the last leaf 10 went to the root and sank 1 level. Each level costs up to 4 comparisons. |
| 20 | L10 | d=4 size=5 | Extract 7: the last leaf 11 went to the root and sank 1 level. Each level costs up to 4 comparisons. |
| 21 | L10 | d=4 size=4 | Extract 8: the last leaf 12 went to the root and sank 1 level. Each level costs up to 4 comparisons. |
| 22 | L10 | d=4 size=3 | Extract 9: the last leaf 10 went to the root and sank 0 levels. Each level costs up to 4 comparisons. |
| 23 | L10 | d=4 size=2 | Extract 10: the last leaf 11 went to the root and sank 0 levels. Each level costs up to 4 comparisons. |
| 24 | L10 | d=4 size=1 | Extract 11: the last leaf 12 went to the root and sank 0 levels. Each level costs up to 4 comparisons. |
| 25 | L1 | d=4 | Sorted output [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]. With d = 4: 13 comparisons for the insertions and 48 for the extractions, 61 in all. |
Binomial heap
A **binomial heap** is a list of **binomial trees**, at most one of each degree, like the binary representation of n. A tree B_k has 2^k nodes and is built by linking two B_(k-1) trees, the smaller key becoming the root. **Merging** two heaps works like binary addition, linking equal-degree trees as carries: O(log n). Insert is a merge with a one-node heap; extract-min removes the smallest root, and its children (which are binomial trees of smaller degrees) are merged back in.
The reward is the fast **meld** (union) that a binary heap lacks, which costs O(n) there. The price is more complex code and more pointers per node.
| Operation | Binomial heap |
|---|---|
| insert | O(log n) worst, O(1) amortised |
| find-min | O(log n) scanning the roots |
| extract-min | O(log n) |
| union | O(log n) |
| decrease-key | O(log n) |
1static class Node {2int key, degree;3Node child, sibling;4Node(int key) { this.key = key; }5}6static Node head; // roots, in increasing order of degree7static void link(Node child, Node parent) { // both trees have the same degree8child.sibling = parent.child;9parent.child = child;10parent.degree++;11}12static Node mergeLists(Node a, Node b) {13Node dummy = new Node(0), t = dummy;14while (a != null && b != null) {15if (a.degree <= b.degree) { t.sibling = a; a = a.sibling; } else { t.sibling = b; b = b.sibling; }16t = t.sibling;17}18t.sibling = a != null ? a : b;19return dummy.sibling;20}21static Node union(Node a, Node b) { // like adding two binary numbers22Node h = mergeLists(a, b);23if (h == null) return null;24Node prev = null, x = h, next = x.sibling;25while (next != null) {26if (x.degree != next.degree || (next.sibling != null && next.sibling.degree == x.degree)) { prev = x; x = next; }27else if (x.key <= next.key) { x.sibling = next.sibling; link(next, x); }28else { if (prev == null) h = next; else prev.sibling = next; link(x, next); x = next; }29next = x.sibling;30}31return h;32}33static void insert(int key) { head = union(head, new Node(key)); }34static int extractMin() {35Node best = head, bestPrev = null, prev = null;36for (Node x = head; x != null; prev = x, x = x.sibling) if (x.key < best.key) { best = x; bestPrev = prev; }37if (bestPrev == null) head = best.sibling; else bestPrev.sibling = best.sibling;38Node rev = null, c = best.child; // the children, reversed into increasing degree order39while (c != null) { Node next = c.sibling; c.sibling = rev; rev = c; c = next; }40head = union(head, rev);41return best.key;42}
Fibonacci heap
A **Fibonacci heap** is a lazy binomial-style heap. Insert just adds a tree to a circular root list: O(1). Union concatenates two root lists: O(1). Only **extract-min** does real work: it moves the minimum's children to the root list and **consolidates**, linking trees of equal degree until all degrees are distinct. **Decrease-key** cuts the node from its parent when heap order breaks, and a **cascading cut** removes marked ancestors to keep the trees bushy, so each costs O(1) amortised.
That makes Dijkstra and Prim run in O(E + V log V), the best known bound, but the constants and pointer overhead are large, so binary heaps (or d-ary heaps) are usually faster in practice. Fibonacci heaps are the best illustration of amortised analysis. The code below is a compact complete version.
1static class Node {2int key, degree;3boolean mark; // lost a child since it last became a child itself4Node parent, child, left = this, right = this;5Node(int key) { this.key = key; }6}7static Node min;8static int size;9static void addRoot(Node x) { // splice x into the circular root list next to min10x.parent = null; x.mark = false;11if (min == null) { x.left = x; x.right = x; min = x; return; }12x.left = min; x.right = min.right; min.right.left = x; min.right = x;13if (x.key < min.key) min = x;14}15static Node insert(int key) { Node x = new Node(key); addRoot(x); size++; return x; }1617static int extractMin() {18Node z = min;19if (z.child != null) { // children become roots20List<Node> kids = new ArrayList<>();21Node c = z.child;22do { kids.add(c); c = c.right; } while (c != z.child);23for (Node k : kids) addRoot(k);24}25z.left.right = z.right; z.right.left = z.left;26if (z == z.right) min = null;27else { min = z.right; consolidate(); }28size--;29return z.key;30}31static void consolidate() {32Node[] table = new Node[64];33List<Node> roots = new ArrayList<>();34Node c = min;35do { roots.add(c); c = c.right; } while (c != min);36for (Node w : roots) {37Node x = w;38int d = x.degree;39while (table[d] != null) { // two trees of degree d: link them into one of degree d + 140Node y = table[d];41if (y.key < x.key) { Node t = x; x = y; y = t; }42link(y, x);43table[d++] = null;44}45table[d] = x;46}47min = null;48for (Node r : table) if (r != null) addRoot(r);49}50static void link(Node y, Node x) { // y becomes a child of x51y.parent = x; y.mark = false;52if (x.child == null) { x.child = y; y.left = y; y.right = y; }53else { y.left = x.child; y.right = x.child.right; x.child.right.left = y; x.child.right = y; }54x.degree++;55}56static void decreaseKey(Node x, int newKey) {57x.key = newKey;58Node p = x.parent;59if (p != null && x.key < p.key) { cut(x, p); cascadingCut(p); } // heap order broke: cut x out60if (x.key < min.key) min = x;61}62static void cut(Node x, Node p) {63if (x.right == x) p.child = null;64else { x.left.right = x.right; x.right.left = x.left; if (p.child == x) p.child = x.right; }65p.degree--;66addRoot(x);67}68static void cascadingCut(Node y) {69Node z = y.parent;70if (z == null) return;71if (!y.mark) y.mark = true; // the first lost child: just mark it72else { cut(y, z); cascadingCut(z); } // the second: cut it too, and continue upward73}
Pairing heap
A **pairing heap** is a heap-ordered tree with a simple rule set: **meld** two heaps by making the one with the larger root a child of the other; insert is a meld with a one-node heap; **delete-min** removes the root and melds its children in **two passes**: first in pairs from left to right, then the results from right to left. It has tiny code and excellent practical speed, often beating Fibonacci heaps, although its exact amortised cost for decrease-key is still an open research question (known bounds are O(log n) amortised for delete-min and insert and meld in O(1)).
The complete implementation fits in a few lines because every operation is a meld.
1static class Node {2int key;3Node child, sibling; // the first child and the next sibling4Node(int key) { this.key = key; }5}6static Node meld(Node a, Node b) {7if (a == null) return b;8if (b == null) return a;9if (b.key < a.key) { Node t = a; a = b; b = t; }10b.sibling = a.child; // the larger root becomes the first child of the smaller11a.child = b;12return a;13}14static Node insert(Node root, int key) { return meld(root, new Node(key)); }15static Node deleteMin(Node root) { // the root's key was the minimum; returns the new root16List<Node> pairs = new ArrayList<>();17Node first = root.child;18while (first != null) { // pass 1: meld the children in pairs, left to right19Node a = first, b = a.sibling;20Node next = b == null ? null : b.sibling;21a.sibling = null;22if (b != null) b.sibling = null;23pairs.add(meld(a, b));24first = next;25}26Node result = null;27for (int i = pairs.size() - 1; i >= 0; i--) result = meld(pairs.get(i), result); // pass 2: right to left28return result;29}
Indexed priority queue
Algorithms such as Dijkstra and Prim need to **change the priority of an item that is already in the queue**. An **indexed priority queue** gives each item a fixed index 0..n-1 and keeps two extra arrays: pq (the heap, holding indexes) and qp (the inverse: qp[i] is the heap position of item i). With qp the position of any item is found in O(1), so decreaseKey(i, key) is a sift-up from qp[i], O(log n), and contains(i) is qp[i] != -1.
It replaces Java's 'push a duplicate and skip stale entries' trick with a true decrease-key, keeping the heap at most n entries, which is better when the number of decrease-keys is large. The cost is the extra bookkeeping: every swap must update qp.
1static class IndexedMinPQ {2private int n; // number of items in the queue3private final int[] pq, qp, key; // pq: heap of item indexes (1-based), qp[i]: position of item i or -14IndexedMinPQ(int capacity) {5pq = new int[capacity + 1]; qp = new int[capacity]; key = new int[capacity];6Arrays.fill(qp, -1);7}8boolean contains(int i) { return qp[i] != -1; }9void insert(int i, int k) { key[i] = k; pq[++n] = i; qp[i] = n; swim(n); }10void decreaseKey(int i, int k) { key[i] = k; swim(qp[i]); }11int delMin() { // returns the index of the smallest key12int min = pq[1];13swap(1, n--);14sink(1);15qp[min] = -1;16return min;17}18int minKey() { return key[pq[1]]; }19boolean isEmpty() { return n == 0; }20private void swim(int p) { while (p > 1 && key[pq[p / 2]] > key[pq[p]]) { swap(p, p / 2); p /= 2; } }21private void sink(int p) {22while (2 * p <= n) {23int c = 2 * p;24if (c < n && key[pq[c + 1]] < key[pq[c]]) c++;25if (key[pq[p]] <= key[pq[c]]) break;26swap(p, c);27p = c;28}29}30private void swap(int x, int y) { int t = pq[x]; pq[x] = pq[y]; pq[y] = t; qp[pq[x]] = x; qp[pq[y]] = y; }31}
Best
O(1)
Average
O(log n)
Worst
O(log n)
Space
O(n)
Decrease-key
**Decrease-key** lowers the priority value of an item already in the heap. In a binary heap the new key can only move **up**, so it is a sift-up from the item's position: O(log n), but you must first **know the position**, which needs the index map of the previous lesson. In the animation's “Decrease a key” preset, the key at index 4 of [1, 3, 5, 7, 9, 8, 6] drops from 9 to 2: it swaps with its parent 3 and stops below the root, 2 comparisons and 1 swap, giving [1, 2, 5, 7, 3, 8, 6].
The heaps differ most in how they handle this operation, which is why Fibonacci heaps matter in theory: decrease-key is O(1) amortised, and it is the operation Dijkstra and Prim perform up to E times.
| Heap | insert | extract-min | decrease-key | meld |
|---|---|---|---|---|
| Binary heap | O(log n) | O(log n) | O(log n) | O(n) |
| d-ary heap | O(log_d n) | O(d log_d n) | O(log_d n) | O(n) |
| Binomial heap | O(1) amortised | O(log n) | O(log n) | O(log n) |
| Fibonacci heap | O(1) | O(log n) amortised | O(1) amortised | O(1) |
| Pairing heap | O(1) | O(log n) amortised | o(log n) amortised, exact bound open | O(1) |
Binary heap: sift-up, sift-down, insert, extract, replace, decrease-key and build-heap
Interactive · edit the inputmin-heap as a tree · 8 nodes
[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
heap array (index = position in the tree)
Heapify 8 values in place: the last 4 entries are leaves, so start at the last internal node and sift each one down, moving toward the root.
1static void siftUp(int[] a, int i) {2while (i > 0) {3int p = (i - 1) / 2; // parent4if (a[p] <= a[i]) break; // heap order restored5swap(a, p, i);6i = p;7}8}9static void siftDown(int[] a, int n, int i) {10while (true) {11int l = 2 * i + 1, r = l + 1, m = i;12if (l < n && a[l] < a[m]) m = l; // find the smallest of the node and its children13if (r < n && a[r] < a[m]) m = r;14if (m == i) break; // both children are larger: done15swap(a, i, m);16i = m;17}18}19static int size;20static void insert(int[] a, int x) { a[size] = x; siftUp(a, size++); }21static int extractMin(int[] a) {22int min = a[0];23a[0] = a[--size]; // move the last leaf to the root24siftDown(a, size, 0);25return min;26}27static void buildHeap(int[] a, int n) {28for (int i = n / 2 - 1; i >= 0; i--) siftDown(a, n, i); // the last n/2 entries are leaves already29}30static int replaceRoot(int[] a, int x) { // pop and push in one sift31int min = a[0];32a[0] = x;33siftDown(a, size, 0);34return min;35}36static void decreaseKey(int[] a, int i, int x) { a[i] = x; siftUp(a, i); }
Live state
size
8
Execution trace (dry run table)Show 24 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | size=8 | Heapify 8 values in place: the last 4 entries are leaves, so start at the last internal node and sift each one down, moving toward the root. |
| 2 | L28 | size=8 index=3 | Sift down the node at index 3 (3). Everything below it is already a heap. |
| 3 | L12 | size=8 node=3 | Compare 3 with its only child 1. The smaller child is 1. |
| 4 | L15 | size=8 node=3 | Swap 3 with 1: the smaller value moves up. |
| 5 | L12 | size=8 node=3 | Compare 3 with its children: none, it is a leaf. Neither is smaller: heap order holds here. |
| 6 | L28 | size=8 index=2 | Sift down the node at index 2 (8). Everything below it is already a heap. |
| 7 | L12 | size=8 node=8 | Compare 8 with its children 2 and 6. The smaller child is 2. |
| 8 | L15 | size=8 node=8 | Swap 8 with 2: the smaller value moves up. |
| 9 | L12 | size=8 node=8 | Compare 8 with its children: none, it is a leaf. Neither is smaller: heap order holds here. |
| 10 | L28 | size=8 index=1 | Sift down the node at index 1 (7). Everything below it is already a heap. |
| 11 | L12 | size=8 node=7 | Compare 7 with its children 1 and 5. The smaller child is 1. |
| 12 | L15 | size=8 node=7 | Swap 7 with 1: the smaller value moves up. |
| 13 | L12 | size=8 node=7 | Compare 7 with its only child 3. The smaller child is 3. |
| 14 | L15 | size=8 node=7 | Swap 7 with 3: the smaller value moves up. |
| 15 | L12 | size=8 node=7 | Compare 7 with its children: none, it is a leaf. Neither is smaller: heap order holds here. |
| 16 | L28 | size=8 index=0 | Sift down the node at index 0 (9). Everything below it is already a heap. |
| 17 | L12 | size=8 node=9 | Compare 9 with its children 1 and 2. The smaller child is 1. |
| 18 | L15 | size=8 node=9 | Swap 9 with 1: the smaller value moves up. |
| 19 | L12 | size=8 node=9 | Compare 9 with its children 3 and 5. The smaller child is 3. |
| 20 | L15 | size=8 node=9 | Swap 9 with 3: the smaller value moves up. |
| 21 | L12 | size=8 node=9 | Compare 9 with its only child 7. The smaller child is 7. |
| 22 | L15 | size=8 node=9 | Swap 9 with 7: the smaller value moves up. |
| 23 | L12 | size=8 node=9 | Compare 9 with its children: none, it is a leaf. Neither is smaller: heap order holds here. |
| 24 | L1 | size=8 heap order=holds | [1, 3, 2, 7, 5, 8, 6, 9]: heap order holds. 11 comparisons and 7 swaps. |
Practice ladder
Now try it yourself
Easy
- Last Stone Weight (with a d-ary heap)
- Kth Smallest Element in a Sorted Matrix
Medium
- Network Delay Time (indexed queue)
- Path With Minimum Effort
Advanced
- Implement a Fibonacci heap
- Minimum Spanning Tree with an indexed priority queue
Key takeaways
- A d-ary heap trades cheaper sift-up for dearer sift-down; larger d suits decrease-key-heavy workloads.
- Binomial heaps meld in O(log n) by adding binary-number-style trees.
- Fibonacci heaps give O(1) amortised insert, meld and decrease-key, with large constants.
- Pairing heaps are tiny and fast in practice; every operation is a meld.
- An indexed priority queue keeps a position map so decrease-key is a sift-up from a known position.