Fibonacci Heap
The Fibonacci heap is a data structure for the mergeable priority queue abstract type: it supports
insert,find-min,decrease-key, andunion(meld) in O(1) amortized, andextract-minanddeletein O(log n) amortized. Introduced by Michael L. Fredman and Robert E. Tarjan in their 1987 Journal of the ACM paper “Fibonacci heaps and their uses in improved network optimization algorithms”, the structure was specifically designed to give the asymptotically fastest known algorithm for single-source shortest paths in dense graphs (O(E + V log V)), minimum spanning tree (O(E + V log V)), and several flow problems. Despite its theoretical elegance and the awe its analysis inspires, Fibonacci heaps are almost never used in practice: the constant factors are large, the pointer-chasing kills cache performance, and the Binary Heap (and even simpler structures like the d-ary heap or pairing heap) consistently win on real hardware. The 2014 Larkin–Sen–Tarjan empirical study — co-authored by Tarjan himself — found Fibonacci heaps to be slower than the simplest implicit binary heap on every workload tested. The Fibonacci heap therefore lives a curious life: a milestone of algorithmic theory, taught as a textbook example of Amortized Analysis using the potential method, central to the asymptotic analysis of classic graph algorithms, and yet a structure most engineers will never deploy.
1. Intuition — The Lazy Office Manager
A standard Binary Heap is the eager office manager: every time someone files a request (insert), the manager immediately reorganizes the entire filing system to keep the highest-priority request on top. That eagerness costs O(log n) per insert.
The Fibonacci heap manager is lazy. When a new request arrives, it gets dropped on the floor next to the others — no reorganization, just a O(1) addition to the pile. When someone needs to decrease a request’s priority (a decrease-key), the manager just snips that one folder out of its current pile and tosses it onto the floor with the rest, also in O(1). The minimum is tracked via a single pointer to whichever folder the manager has noticed is smallest so far.
The bill comes due during extract-min: when someone actually asks for the highest-priority request, now the manager finally has to organize. They sweep the floor, pair up trees of equal “rank” (binomial-style), pair the resulting larger trees, and so on, until at most one tree of each rank remains. That sweep costs O(log n) amortized — but the analysis ensures it’s O(log n) on average per extract, even though one particular extract may scan many trees.
The Fibonacci-Heap insight, the part that sets it apart from the simpler Binomial Heap (which already supports O(1) merge), is the decrease-key operation. Decrease-key in a binomial heap requires a sift-up that costs O(log n). In a Fibonacci heap, you cut the affected node out of its current tree and graft it onto the root list — one pointer flip, O(1). To prevent the trees from becoming arbitrarily unbalanced over many decrease-keys, the structure introduces a mark bit on each node and a cascading-cut rule: a node loses at most two children before it itself is cut out. This rule is the heart of the analysis, and the reason Fibonacci numbers appear in the name (the minimum number of descendants of a node of rank k turns out to be F_{k+2}, the (k+2)-nd Fibonacci number).
2. The Structure
2.1 What a Fibonacci Heap Looks Like
A Fibonacci heap H is a collection of heap-ordered trees (each parent ≤ each child) whose roots form a circular doubly-linked root list. The heap maintains one pointer, H.min, to the node with the smallest key (the global minimum, always a root).
Each node x stores:
key[x]— the priority valuedegree[x]— the number of children of xparent[x],child[x]— pointers (the children form a circular doubly-linked list, accessed through any one)left[x],right[x]— sibling pointers in the doubly-linked list it lives in (root list, or its parent’s child list)mark[x]— a single bit, true iff x has lost a child since the last time x became the child of another node
The data structure is a forest of heap-ordered trees, with the trees connected only via the root list — not a single tree like a binary heap. Each tree can have arbitrary shape (not necessarily binomial-shaped, even though the analysis reasons about ranks the same way).
Root list (circular doubly-linked, accessed via H.min):
H.min → 3 ↔ 7 ↔ 11 ↔ 5 ↔ (back to 3)
| | |
5 12 8 ↔ 9
| |
8 17
2.2 The Core Invariants
- Heap order:
key[parent] ≤ key[child]for every parent-child pair. - Mark discipline: when a non-root node x has its second child cut (x lost two children since x last became a child), x is itself cut out and added to the root list (and unmarked).
- Rank bound: for any node x of rank (= degree) k, the subtree rooted at x has at least
F_{k+2}nodes, whereF_nis the n-th Fibonacci number. SinceF_{k+2} ≥ φ^kforφ = (1+√5)/2 ≈ 1.618, this meansk ≤ log_φ(n), so the maximum degree of any node is O(log n).
The third invariant is what bounds extract-min’s amortized cost. The proof — that mark-and-cascade cuts force the Fibonacci-numbered subtree-size lower bound — is the technical core of the original Fredman-Tarjan paper and is reproduced in CLRS Lemma 19.1 and 19.4. We sketch it in §6.
3. Tiny Worked Example
Insert keys 7, 3, 11, 5, 12, 8, 9, 17 one by one, then extract-min.
After all eight inserts (no consolidation has happened yet — every insert is just “drop on root list, update H.min”):
Root list: 7 ↔ 3 ↔ 11 ↔ 5 ↔ 12 ↔ 8 ↔ 9 ↔ 17
H.min → 3
All eight nodes are individual roots, all with degree 0, none marked. The H.min pointer is at 3.
Now call extract-min():
Step A — extract the minimum. Remove 3 from the root list. (3 has no children to splice in; if it had, they’d be added to the root list now.) Remaining root list: 7 ↔ 11 ↔ 5 ↔ 12 ↔ 8 ↔ 9 ↔ 17. Returned: 3.
Step B — consolidate. Walk the root list, building an array A indexed by degree. For each root x of degree d, if A[d] is empty, set A[d] := x. Otherwise, link the larger-keyed of A[d] and x as a child of the smaller, producing a tree of degree d+1; set A[d] := nil, then continue trying to place the new larger tree at index d+1. Continue until every root has been placed in A.
Walking 7, 11, 5, 12, 8, 9, 17 (all degree 0):
| root | conflict at A[0]? | Action | A after |
|---|---|---|---|
| 7 | empty | A[0] = 7 | A[0]=7 |
| 11 | A[0]=7 | link 11 under 7 (since 7 < 11) → tree(7→11) of degree 1; A[0]=nil; place at A[1] | A[1]=tree(7→11) |
| 5 | A[0] empty | A[0]=5 | A[0]=5, A[1]=tree(7→11) |
| 12 | A[0]=5 | link 12 under 5 → tree(5→12); A[0]=nil; A[1] occupied (tree(7→11)) → link tree(5→12) under tree(7→11)? No: 5 < 7, so link tree(7→11) under tree(5→12). The new tree has degree 2, root 5, children 7 and 12, with 7 still parent of 11. A[1]=nil. Place at A[2]. | A[2]=tree(5→[7→11, 12]) |
| 8 | A[0] empty | A[0]=8 | A[0]=8, A[2]=tree(5→[7→11, 12]) |
| 9 | A[0]=8 | link 9 under 8 → tree(8→9); A[0]=nil; A[1] empty → A[1]=tree(8→9) | A[0]=nil, A[1]=tree(8→9), A[2]=tree(5→[7→11, 12]) |
| 17 | A[0] empty | A[0]=17 | A[0]=17, A[1]=tree(8→9), A[2]=tree(5→[7→11, 12]) |
After consolidation, the root list is 5 ↔ 8 ↔ 17 (in some order — depends on traversal). Three trees, of degrees 2, 1, 0 respectively. H.min = 5.
This example shows the lazy → consolidate → bounded-degree pattern: inserts are pure O(1), and the cleanup happens all at once on extract-min. The Fibonacci-numbered bound F_{k+2} says that a tree of degree k has at least F_{k+2} nodes, so the consolidation array has at most log_φ(n)+1 slots — bounding the work.
4. Pseudocode
class Node:
key, degree, mark, parent, child, left, right
class FibHeap:
min : Node # null if empty
n : int # total node count
# All operations assume circular doubly-linked lists; "splice" is O(1).
insert(H, x):
x.degree := 0
x.parent := nil
x.child := nil
x.mark := False
add x to H's root list (splice into H.min's list)
if H.min is nil or x.key < H.min.key:
H.min := x
H.n := H.n + 1
# Cost: O(1) actual, O(1) amortized.
find_min(H):
return H.min # O(1) actual.
union(H1, H2):
H := new FibHeap
H.min := smaller-keyed of (H1.min, H2.min)
concatenate H1 and H2 root lists # O(1) for circular doubly-linked
H.n := H1.n + H2.n
return H
# O(1) actual, O(1) amortized.
extract_min(H):
z := H.min
if z != nil:
for each child c of z:
add c to root list of H
c.parent := nil
remove z from root list
if z == z.right: # z was the only root
H.min := nil
else:
H.min := z.right # placeholder; consolidate fixes it
consolidate(H)
H.n := H.n - 1
return z
# O(D(n)) amortized, where D(n) = O(log n) is the max degree.
consolidate(H):
let A be an array of size D(H.n)+1, all nil
for each root w in H's root list (snapshot first):
x := w
d := x.degree
while A[d] != nil:
y := A[d]
if x.key > y.key:
swap(x, y) # x always has smaller key
link(y, x) # y becomes child of x; x.degree++
A[d] := nil
d := d + 1
A[d] := x
H.min := nil
for i in 0 to D(H.n):
if A[i] != nil:
add A[i] to root list of H
if H.min == nil or A[i].key < H.min.key:
H.min := A[i]
link(y, x):
remove y from root list
make y a child of x; x.degree += 1
y.mark := False
decrease_key(H, x, k):
if k > x.key: error "new key larger"
x.key := k
y := x.parent
if y != nil and x.key < y.key:
cut(H, x, y)
cascading_cut(H, y)
if x.key < H.min.key:
H.min := x
# O(1) amortized.
cut(H, x, y):
remove x from y's child list
y.degree -= 1
add x to root list of H
x.parent := nil
x.mark := False
cascading_cut(H, y):
z := y.parent
if z != nil:
if y.mark == False:
y.mark := True
else:
cut(H, y, z)
cascading_cut(H, z)
# The recursion is O(1) amortized (potential-method analysis below).
delete(H, x):
decrease_key(H, x, -infinity)
extract_min(H)
# O(log n) amortized.
5. Python Implementation
A teaching-quality reference implementation. It is not optimized for production — for that, you’d want struct-of-arrays layout, manual memory pooling, and ideally a different language. This version focuses on correctness and pedagogical clarity.
from __future__ import annotations
from dataclasses import dataclass, field
from math import log, sqrt
from typing import Optional, Iterator
@dataclass
class _FibNode:
"""Node in a Fibonacci heap.
Children form a circular doubly-linked list; same for the root list. We
keep a `child` pointer to *any one* child (entry into the cycle).
"""
key: int | float
parent: Optional["_FibNode"] = None
child: Optional["_FibNode"] = None
left: "_FibNode" = field(init=False)
right: "_FibNode" = field(init=False)
degree: int = 0
mark: bool = False
def __post_init__(self) -> None:
# A new node is its own predecessor and successor (a singleton ring).
self.left = self
self.right = self
class FibonacciHeap:
"""Min-Fibonacci heap. Supports insert/find_min/decrease_key/meld in O(1)
amortized; extract_min and delete in O(log n) amortized."""
def __init__(self) -> None:
self.min: Optional[_FibNode] = None
self.n: int = 0
# --- public API ---
def insert(self, key: int | float) -> _FibNode:
node = _FibNode(key=key)
self._add_to_root_list(node)
if self.min is None or key < self.min.key:
self.min = node
self.n += 1
return node
def find_min(self) -> int | float:
if self.min is None:
raise IndexError("find_min on empty heap")
return self.min.key
def union(self, other: "FibonacciHeap") -> "FibonacciHeap":
"""Merge `other` into `self` and return self (in-place meld)."""
if other.min is None:
return self
if self.min is None:
self.min, self.n = other.min, other.n
return self
# Splice the two circular doubly-linked root lists into one.
# self's list: ... A ↔ self.min ↔ B ...
# other's list: ... C ↔ other.min ↔ D ...
# After splice: ... A ↔ self.min ↔ D ... C ↔ other.min ↔ B ...
a, b = self.min, self.min.right
c, d = other.min, other.min.right
a.right = d; d.left = a
c.right = b; b.left = c
if other.min.key < self.min.key:
self.min = other.min
self.n += other.n
return self
def extract_min(self) -> int | float:
z = self.min
if z is None:
raise IndexError("extract_min on empty heap")
# Splice each child of z into the root list.
if z.child is not None:
children = list(self._iter_circular(z.child))
for c in children:
c.parent = None
# Splice the entire children-cycle into the root list before z.
# (Fast: rewire four pointers.)
head = z.child
head_left = head.left
z_left = z.left
z_left.right = head; head.left = z_left
head_left.right = z; z.left = head_left
z.child = None
# Now remove z itself from the root list.
z.left.right = z.right
z.right.left = z.left
if z is z.right:
self.min = None
else:
self.min = z.right
self._consolidate()
self.n -= 1
return z.key
def decrease_key(self, x: _FibNode, k: int | float) -> None:
if k > x.key:
raise ValueError("new key is greater than current key")
x.key = k
y = x.parent
if y is not None and x.key < y.key:
self._cut(x, y)
self._cascading_cut(y)
assert self.min is not None
if x.key < self.min.key:
self.min = x
def delete(self, x: _FibNode) -> None:
self.decrease_key(x, float("-inf"))
self.extract_min()
# --- internals ---
def _add_to_root_list(self, x: _FibNode) -> None:
x.parent = None
if self.min is None:
x.left = x.right = x
else:
x.right = self.min.right
x.left = self.min
self.min.right.left = x
self.min.right = x
@staticmethod
def _iter_circular(start: _FibNode) -> Iterator[_FibNode]:
if start is None:
return
node = start
while True:
yield node
node = node.right
if node is start:
return
def _consolidate(self) -> None:
# Maximum possible degree D(n) is O(log_φ n). Use ceil(log_φ n) + 2.
phi = (1.0 + sqrt(5.0)) / 2.0
max_degree = int(log(max(self.n, 2), phi)) + 2
A: list[Optional[_FibNode]] = [None] * (max_degree + 1)
# Snapshot the root list because we'll be mutating it as we link.
assert self.min is not None
roots = list(self._iter_circular(self.min))
for w in roots:
x = w
d = x.degree
while d < len(A) and A[d] is not None:
y = A[d]
assert y is not None
if x.key > y.key:
x, y = y, x
self._link(y, x)
A[d] = None
d += 1
if d >= len(A):
A.extend([None] * (d - len(A) + 1))
A[d] = x
# Rebuild root list from A.
self.min = None
for node in A:
if node is None:
continue
# Detach into a singleton ring then splice via _add_to_root_list.
node.left = node.right = node
self._add_to_root_list(node)
if self.min is None or node.key < self.min.key:
self.min = node
def _link(self, y: _FibNode, x: _FibNode) -> None:
# Remove y from root list.
y.left.right = y.right
y.right.left = y.left
# Make y a child of x.
if x.child is None:
y.left = y.right = y
x.child = y
else:
y.right = x.child.right
y.left = x.child
x.child.right.left = y
x.child.right = y
y.parent = x
x.degree += 1
y.mark = False
def _cut(self, x: _FibNode, y: _FibNode) -> None:
# Remove x from y's child list.
if x.right is x:
y.child = None
else:
x.left.right = x.right
x.right.left = x.left
if y.child is x:
y.child = x.right
y.degree -= 1
# Add x to root list.
x.left = x.right = x
self._add_to_root_list(x)
x.parent = None
x.mark = False
def _cascading_cut(self, y: _FibNode) -> None:
z = y.parent
if z is not None:
if not y.mark:
y.mark = True
else:
self._cut(y, z)
self._cascading_cut(z)A small sanity test exercising the headline operations:
def _demo() -> None:
h = FibonacciHeap()
nodes = {k: h.insert(k) for k in [7, 3, 11, 5, 12, 8, 9, 17]}
assert h.find_min() == 3
assert h.extract_min() == 3
assert h.find_min() == 5
h.decrease_key(nodes[12], 1) # 12 → 1, now the new min
assert h.find_min() == 1
assert h.extract_min() == 1
assert h.extract_min() == 5
if __name__ == "__main__":
_demo()The implementation is over 100 lines despite being intentionally minimal — much longer than Binary Heap’s ~30 lines. This length is one of the practical reasons Fibonacci heaps are rarely chosen.
6. Amortized Complexity Analysis (Potential Method)
The Fibonacci heap is the textbook setting for the potential method of amortized analysis. We define a potential function Φ(H) on the state of the heap and show that the amortized cost ĉ_i = c_i + Φ_i − Φ_{i−1} is small for each operation, even when the actual cost c_i is occasionally large.
6.1 Potential Function
Let t(H) be the number of trees in the root list of H, and m(H) be the number of marked nodes (nodes that are not roots and have lost one child). Define:
Φ(H) = t(H) + 2 · m(H)
Φ ≥ 0 always, and Φ = 0 for an empty heap, so Σ ĉ_i ≥ Σ c_i — proving amortized bounds proves real ones.
The +2 · m(H) weighting is what makes cascading cut cheap in amortization: each marked node carries 2 units of potential, ready to pay for the moment it triggers a cascading cut.
6.2 Insert
c_i = O(1) — drop into the root list. Φ increases by 1 (one new tree), so ĉ_i = O(1) + 1 = O(1). Done.
6.3 Union
c_i = O(1) — concatenate two circular root lists. Φ doesn’t change (number of trees + marked nodes is preserved). ĉ_i = O(1). Done.
6.4 Find-Min
c_i = O(1), Φ doesn’t change. ĉ_i = O(1).
6.5 Extract-Min
The actual cost c_i = O(degree(min)) + O(t), where t is the number of roots after we’ve added the children of min: O(degree(min)) for splicing children and O(t) for consolidation.
After consolidation, the heap has at most D(n)+1 trees (where D(n) is the maximum possible degree), because the consolidation array has D(n)+1 slots. So t' ≤ D(n)+1.
The change in potential: ΔΦ = (D(n)+1) − t + 0, since marks only decrease (newly-promoted-to-root nodes are unmarked) and we conservatively use 0 for the change in m.
Amortized cost:
ĉ_i = c_i + ΔΦ
= O(degree(min) + t) + (D(n)+1 − t)
= O(D(n) + t) + D(n) + 1 − t
= O(D(n))
= O(log n)
(using D(n) = O(log n), proved in §6.7).
The key cancellation is that the actual cost’s O(t) term is paid for by the −t drop in potential — every tree that gets consumed by consolidation released a unit of potential that funded the work.
6.6 Decrease-Key (and Cascading Cut)
Let c be the number of cuts (one initial cut + cascading cuts). Actual cost: c_i = O(c).
Potential change:
tincreases byc(each cut creates a new root). Contribution:+c.- Each non-final cascading cut removes one mark (it cuts a marked node), and the final cut may add a mark on the new parent if the parent isn’t a root. So
mchanges by at most−(c−1) + 1 = −c + 2. Contribution to potential:2 · (−c + 2) = −2c + 4.
So ΔΦ ≤ c + (−2c + 4) = 4 − c.
Amortized cost:
ĉ_i = c_i + ΔΦ ≤ O(c) + 4 − c = O(1)
The −c from the demarking of cascading-cut nodes exactly cancels the +c actual cost. Mark bits are “savings accounts”: each marked node has 2 units of potential ready to pay the costs of its cut and the cascade it triggers.
This is the most beautiful piece of the analysis: the 2 · m(H) weighting was engineered to make this calculation work out. It’s why the constant 2 is in the potential function — not because of any deep mathematical reason, but because the cascading-cut operation removes one mark per cut and adds at most one mark per call.
6.7 The D(n) = O(log n) Bound — Where Fibonacci Numbers Appear
Lemma (Fredman-Tarjan 1987, CLRS Lemma 19.1). Let x be any node in a Fibonacci heap, and let y_1, y_2, ..., y_k be its children in the order they were linked to x. Then degree(y_i) ≥ i − 2 for i ≥ 2, and degree(y_1) ≥ 0.
Why: when y_i was linked to x (during a consolidation), x already had children y_1, …, y_{i−1} (so x had degree at least i−1 at the time of the linking). Consolidation only links two trees of equal degree, so y_i had degree ≥ i−1 at the moment of linking. After linking, y_i may have lost up to one child without being cut (because the mark rule allows up to one loss before a cascading cut). So today, degree(y_i) ≥ (i−1) − 1 = i − 2.
Lemma (CLRS Lemma 19.4). Let s_k denote the minimum number of nodes in any subtree rooted at a node of degree k. Then s_k ≥ F_{k+2}, the (k+2)-nd Fibonacci number.
Proof sketch. Recurrence: s_k ≥ 2 + s_0 + s_1 + ... + s_{k-2} (the 2 comes from the node itself and y_1; the rest from the recurrence applied to y_2, ..., y_k). The Fibonacci identity F_{k+2} = 1 + F_0 + F_1 + ... + F_k gives the matching solution.
Corollary. Since F_{k+2} ≥ φ^k for φ = (1+√5)/2, and since a heap of size n has every subtree of size ≤ n, every node has degree at most log_φ(n) ≈ 1.4404 · log_2(n). So D(n) = O(log n). ∎
This is where the name comes from: the structural invariant guaranteed by mark-and-cascade-cut produces minimum subtree sizes that follow the Fibonacci recurrence.
7. Comparison with Other Heaps
| Operation | Binary Heap | Binomial Heap | Fibonacci Heap | Pairing Heap (typical) |
|---|---|---|---|---|
| insert | O(log n) | O(log n) worst, O(1) amort. | O(1) | O(1) |
| find-min | O(1) | O(log n) (or O(1) with cached pointer) | O(1) | O(1) |
| extract-min | O(log n) | O(log n) | O(log n) amort. | O(log n) amort. |
| decrease-key | O(log n) | O(log n) | O(1) amort. | O(log n) ≤ amort. ≤ O(2^{2√log log n}) — see Iacono–Özkan 2014 |
| union (meld) | O(n) | O(log n) | O(1) amort. | O(1) |
| typical real-world speed | fastest | slow | slowest | fast on some workloads |
The “fastest in practice” annotation reflects empirical findings — most prominently the Larkin, Sen & Tarjan 2014 ALENEX study, which directly compared d-ary, Fibonacci, pairing, and several other heaps across realistic workloads (including the Dijkstra’s Algorithm use case Fibonacci heaps were designed for). The result:
“The d-ary heaps performed best on each of the workloads we tested … we found no workload on which the Fibonacci heap was the fastest.” — Larkin, Sen, & Tarjan 2014, §6.
The reasons Fibonacci heaps lose in practice despite better asymptotics:
- Cache locality. A Fibonacci heap is a forest of nodes connected by pointers. Every operation touches multiple cache lines. A binary heap is an array — all operations stay within a few cache lines, and modern CPU prefetchers love it. Cache-miss costs (typically 100–300 cycles) dominate over the asymptotic difference, which only kicks in for enormous n.
- Large constants in the analysis. The hidden constants for Fibonacci heap operations are 4–8× the constants for binary heap operations. Even when the Fibonacci heap should asymptotically win, the crossover point is in the millions of operations or more.
- Memory overhead. Each node has 5+ pointers (parent, child, left, right, plus the key and metadata). A binary heap stores only the key. For 10^6 ints, the Fibonacci heap uses roughly 10× the RAM.
- Implementation complexity. A correct Fibonacci heap is several hundred lines of code; bugs are hard to find. A binary heap is 30 lines and famously bug-prone in only the most well-known ways.
- In algorithms like Dijkstra, the bottleneck is rarely heap operations. It’s edge relaxations or graph representation. The asymptotic improvement of
O(E + V log V)vsO((E + V) log V)only matters whenE ≫ V log V, which is uncommon for the dense graphs supposedly favoring Fibonacci heaps. Real-world graphs are sparse; the binary heap’s better constants dominate.
The 2014 study includes Tarjan as co-author — a remarkable example of the inventor of an algorithm publicly demonstrating its real-world inferiority to simpler alternatives. The Fibonacci heap remains the asymptotic standard for theory; it is essentially never the right engineering choice.
8. Use Cases — Where Fibonacci Heaps Are Cited
Even though they are rarely deployed, Fibonacci heaps drive the worst-case analysis of several classical algorithms:
8.1 Dijkstra’s Algorithm
Using a Fibonacci heap as the priority queue makes Dijkstra’s Algorithm run in O(E + V log V). Each edge relaxation potentially triggers a decrease-key (O(1) amortized), and there are at most E of them. Each extract-min is O(log V), executed V times. Total: O(E·1 + V·log V) = O(E + V log V). With a binary heap, decrease-key is O(log V), so the total becomes O((E + V) log V). The improvement is meaningful only when E = ω(V log V) — i.e., on graphs denser than Θ(V log V) edges. For sparse graphs (most real graphs), there is no asymptotic advantage.
8.2 Prim’s Minimum Spanning Tree
Same analysis applies to Prim’s Algorithm: O(E + V log V) with Fibonacci heap; O((E + V) log V) with binary heap.
8.3 Network Flow Algorithms
Fredman and Tarjan’s original paper showed Fibonacci heaps improve several flow problems: the all-pairs shortest paths reduction via Johnson’s Algorithm becomes O(VE + V² log V), and the assignment problem and the minimum-cost flow problem similarly benefit.
8.4 Theoretical Frontiers
Some algorithms with delicate amortized analyses depend on the specific structure of Fibonacci heaps (or their close relatives like the rank-pairing heap and the strict Fibonacci heap of Brodal-Lagogiannis-Tarjan 2012). For the asymptotic best-known bounds for graph algorithms in general, knowing Fibonacci heaps exist is necessary even if you never type one out.
8.5 Why It’s Still Worth Learning
Despite the practical irrelevance, the analysis of the Fibonacci heap is one of the most beautiful examples of the potential method, and the structure has inspired generations of priority queue research. The pairing heap, the strict Fibonacci heap, the rank-pairing heap, the Brodal queue — all owe their existence to the Fibonacci heap. If you understand Φ = t + 2m and the cascading-cut argument, you have the foundations to read the literature on persistent priority queues, decrease-key data structures, and worst-case priority queues.
9. Pitfalls
- Mistaking lazy operations for free. Insert and decrease-key are O(1) per operation, but the cumulative work of many of them is paid for during the next extract-min. The system has work in flight; you can’t avoid that work, only defer it.
- Forgetting the mark-bit discipline. A Fibonacci heap without cascading cuts is a binomial heap-ish structure that has decrease-key cost O(log n) — losing the headline feature. The two-children-loss rule is non-negotiable.
- Confusing rank with node count. Rank (= degree, = number of children) is at most
O(log n), but the subtree size is at leastF_{rank+2}and can be much larger. The analysis uses both. - Assuming Fibonacci heaps beat binary heaps in practice. They don’t. Almost ever. Cite the Larkin–Sen–Tarjan 2014 study before any production decision.
- Implementing without circular doubly-linked lists. Naïve singly-linked or non-circular implementations turn the O(1) splice operations into O(n) traversals, destroying the entire complexity argument.
- Recursion depth in cascading cut. For a teaching implementation, recursive cascading cut is fine on inputs that won’t overflow the stack. For “real” use, convert to iteration. The amortized analysis is unaffected by the iterative form.
extract_minnot consolidating completely. If you forget the consolidation step, the root list grows without bound andextract_minbecomes Θ(t) where t is the number of roots — destroying the amortized bound. The consolidation is what trades the deferred work back for the amortized guarantee.- Memory usage in deep workloads. For very large queues, the per-node overhead matters: 5 pointers plus a key plus mark = 50 bytes per node typical. That’s 10–20× a binary heap’s 8-byte slot. On large graphs, you may run out of RAM before you exhaust CPU.
- Not handling the empty-heap edge case in
_consolidate. Ifself.minis null, the consolidation loop iterates over nothing and there’s nothing to fix; but if the loop is mis-guarded, you’ll dereference null. Always checkself.min is Nonebefore consolidating. - Using a Fibonacci heap when the algorithm doesn’t actually call decrease-key. For pure insert/extract-min workloads, a Fibonacci heap is strictly worse than a binary heap. Decrease-key is the only reason it exists.
10. Diagram — Tree-Linking During Consolidation
flowchart TD subgraph S0["Before extract_min: 8 singleton trees"] R1["7"] R2["3 (min)"] R3["11"] R4["5"] R5["12"] R6["8"] R7["9"] R8["17"] end subgraph S1["After extract 3, before consolidation: 7 singleton trees"] T1["7"] T2["11"] T3["5"] T4["12"] T5["8"] T6["9"] T7["17"] end subgraph S2["After consolidation: 3 trees, degrees 0,1,2"] U1["17 (deg 0)"] U2a["8 (deg 1)"] U2b["8 → 9"] U3a["5 (deg 2)"] U3b["5 → 7, 12"] U3c["7 → 11"] U2a --- U2b U3a --- U3b U3a --- U3c end S0 --> S1 --> S2
What this diagram shows. Three snapshots of the same Fibonacci heap from §3:
- S0: before any consolidation has occurred. Every insert was a pure O(1) drop into the root list. There are 8 singleton trees (degree 0); H.min points to 3.
- S1: after
extract_minhas removed 3, but before consolidation. There are 7 singleton trees. The root list is “messy” — many trees of the same rank. - S2: after the consolidate step has paired equal-rank trees, and the trees have been linked into larger heap-ordered trees. There are now exactly 3 trees, of degrees 0, 1, and 2 — at most one tree per degree.
The progression demonstrates the lazy-then-consolidate pattern: cheap inserts and decrease-keys accumulate trees and marked nodes; extract-min triggers a single sweeping pass that brings the structure back into a “regular” shape with at most O(log n) trees. The Fibonacci-numbered subtree-size guarantee is what bounds how many trees S2 can contain — it can have a degree-k tree only if that tree has at least F_{k+2} nodes, so a heap of n nodes has at most O(log_φ n) trees after consolidation.
11. Common Interview Problems
| Context | Connection |
|---|---|
| Asymptotic analysis of Dijkstra’s Algorithm | ”Why is Dijkstra O(E + V log V) instead of O((E + V) log V)?” — answer mentions Fibonacci heap |
| Asymptotic analysis of Prim’s Algorithm | Same question, same answer |
| Comparison of priority queue ADTs | ”Walk me through the trade-offs of Binary Heap vs binomial vs Fibonacci vs pairing heap” |
| Amortized-analysis pedagogy | ”Explain the potential method using Fibonacci heap as the example” |
| Theoretical bound on decrease-key | ”Is there a worst-case O(1) decrease-key heap?” → strict Fibonacci heap (Brodal-Lagogiannis-Tarjan 2012) |
Implementing a Fibonacci heap in an interview is rare. The structure is so elaborate that interviewers reserve it for graduate-level or research-track interviews, or use it only as a discussion topic (“describe the structure and its analysis”). The take-home value is to be able to explain the operations, the analysis, and especially why no one uses it in practice.
12. Open Questions
- Does the strict Fibonacci heap (Brodal, Lagogiannis & Tarjan 2012, “Strict Fibonacci heaps”, STOC) — which achieves all bounds in worst case rather than amortized — see any practical use? Probably not, but I haven’t seen the empirical comparison.
- Is there a CPU-cache-friendly redesign of the Fibonacci heap that retains its asymptotic guarantees? The literature on “implicit” or “array-based” Fibonacci heaps is sparse; whether such a redesign is even possible is open.
- What’s the true state of the pairing heap’s decrease-key amortized cost? Iacono and Özkan 2014 proved an upper bound of
O(2^{2√log log n}); Pettie 2005 proved anΩ(log log n)lower bound; the gap is still open as of the literature I’ve read. - When does the asymptotic gap between Fibonacci heap and binary heap actually matter on real hardware? The crossover seems to be at n in the billions for typical operations, but I’ve not seen a clean experimental demonstration.
Verify on a future pass
The Iacono-Özkan 2014 bound on pairing heap decrease-key is the best I’m aware of, but the field moves; check if a tighter result has been published.
13. See Also
- Binary Heap — the simple, fast, almost-always-better alternative
- Binomial Heap — the predecessor that provided most of the structural ideas (planned)
- Dijkstra’s Algorithm — the headline application giving
O(E + V log V)with a Fibonacci heap - Prim’s Algorithm — same speedup
- Johnson’s Algorithm — uses Fibonacci heap to run V Dijkstras for all-pairs shortest paths
- Amortized Analysis — the formal framework for the analysis here (planned); the potential method is the centerpiece
- Big-O Notation
- SWE Interview Preparation MOC