Fibonacci Heap
A collection of heap-ordered trees with lazy consolidation giving amortized O(1) insert, merge, and decrease-key, and O(log n) extract-min.
Definition
A Fibonacci heap is a min-priority-queue built from a forest of heap-ordered trees linked in a circular doubly linked root list, plus a pointer to the minimum root. Its claim to fame is amortized O(1) for insert, merge, and decrease-key, with O(log n) amortized extract-min.
The payoff is asymptotic: Dijkstra's Algorithm drops from O((V + E) log V) with a Binary Heap to O(E + V log V), and Prim's Algorithm likewise, because the E decrease-key calls become O(1) each. In practice constant factors and pointer chasing make binary and pairing heaps faster on almost all real inputs.
The structure is lazy: insert just adds a one-node tree to the root list; the actual work — linking trees of equal degree — is deferred until extract-min. The name comes from the bound on tree size: a tree of degree k has at least F(k+2) nodes, where F is the Fibonacci sequence.
Intuition
A mental model before the formal terms.
Imagine a desk where incoming papers are dropped into separate piles instead of being filed. When you need the most urgent paper (the minimum), you finally tidy: you merge piles of the same size, two at a time, so that afterward there is at most one pile per size. Dropping (insert) is free; tidying (extract-min) is paid rarely and cleans up all the accumulated mess at once.
Decrease-key is a quick fix: if a paper becomes more urgent than the pile it sits under, you just rip it out and put it in its own pile at the top. To stop piles from being hollowed out too much, each pile tracks whether it already lost a child ("marked"); losing a second child makes the pile itself get ripped out too (cascading cut).
How it works
- Node:
key,degree(number of children),mark, and pointersparent,child,left,right(circular sibling list). - Insert(x): create a single-node tree, splice it into the root list, update
minif smaller.O(1). - Merge(H1, H2): concatenate the two root lists, keep the smaller
min.O(1). - Extract-min: remove
min, promote its children to the root list, then consolidate: walk the root list with a degree tableA[0..log n]; whenever two roots have the same degree, link the larger under the smaller (degree + 1) and repeat until every degree is unique. Finally rescan roots for the newmin. AmortizedO(log n). - Decrease-key(x, k): set
x.key = k. Ifxnow violates heap order with its parentp, cutx(move it to the root list, clear its mark). Ifpwas already marked, cutptoo, and continue upward (cascading cut); otherwise markp. AmortizedO(1). - Delete(x): decrease-key to
-∞, then extract-min.
Why it works
Potential function Φ = t + 2m where t is the number of root trees and m the number of marked nodes. Insert raises Φ by 1 (paying for a future link); each link during consolidation lowers t by 1 and pays for itself; each cascading cut lowers m and pays for the next cut.
The mark rule guarantees every node loses at most one child before it is itself cut, so a node of degree k has a subtree of size at least F(k+2) ≥ φ^k. Hence max degree is O(log n), so the degree table and extract-min stay O(log n).
Consolidation is needed only after extract-min; there min had O(log n) children, and the number of trees before consolidation is bounded by prior inserts already paid for by the potential.
Operations
| Operation | Description | Cost |
|---|---|---|
| insert | Add a single-node tree to the root list. | O(1) amortized |
| find-min | Return the min pointer. | O(1) |
| merge / union | Splice two root lists together. | O(1) amortized |
| extract-min | Remove min, promote children, consolidate by degree. | O(log n) amortized |
| decrease-key | Cut the node to the root list; cascade through marked ancestors. | O(1) amortized |
| delete | Decrease-key to -∞ then extract-min. | O(log n) amortized |
Recognition
How to tell a problem wants this.
- A theoretical question: "what is the best known bound for Dijkstra / Prim on dense graphs?" —
O(E + V log V)is the Fibonacci-heap bound. - The workload is dominated by decrease-key or merge rather than extract-min.
- In interviews it appears almost exclusively as a discussion topic about amortized analysis and priority-queue trade-offs, not as something to implement.
Interactive demo
Play, step, change the input. ← → and space work too.
No interactive visualization for this topic yet
Related visualizations are linked under Related.
Pseudocode
1insert(x): add x to root list; if x.key < min.key: min = x2extract_min():3 z = min; move each child of z to root list; remove z4 consolidate(): A = degree table5 for each root w: while A[w.degree] exists: link larger under smaller; A[old] = null; A[w.degree] = w6 min = smallest root7decrease_key(x, k): x.key = k; p = x.parent8 if p and x.key < p.key: cut(x, p); cascading_cut(p)9cascading_cut(y): while y.parent: if not y.mark: y.mark = true; return; else cut(y, parent); y = parentImplementation
1from __future__ import annotations2 3 41 · Node structure and heap state5class FibNode:6 __slots__ = ("key", "degree", "mark", "parent", "child", "left", "right")7 8 def __init__(self, key: int):9 self.key = key10 self.degree = 011 self.mark = False12 self.parent: FibNode | None = None13 self.child: FibNode | None = None14 self.left: FibNode = self # circular doubly linked sibling list15 self.right: FibNode = self16 17 18class FibonacciHeap:19 def __init__(self):20 self._min: FibNode | None = None # pointer into the root list21 self._n = 022 23 @staticmethod24 def _splice_in(at: FibNode, x: FibNode) -> None:25 x.left = at26 x.right = at.right27 at.right.left = x28 at.right = x29 30 @staticmethod31 def _unlink(x: FibNode) -> None:32 x.left.right = x.right33 x.right.left = x.left34 x.left = x.right = x35 362 · Insert and merge (lazy — just extend the root list)37 def insert(self, key: int) -> FibNode:38 x = FibNode(key)39 if self._min is None:40 self._min = x41 else:42 self._splice_in(self._min, x)43 if x.key < self._min.key:44 self._min = x45 self._n += 146 return x # handle needed later for decrease_key47 48 def merge(self, other: "FibonacciHeap") -> None:49 if other._min is None:50 return51 if self._min is None:52 self._min = other._min53 else:54 a, b = self._min.right, other._min.right55 self._min.right, b.left = b, self._min56 other._min.right, a.left = a, other._min57 if other._min.key < self._min.key:58 self._min = other._min59 self._n += other._n60 other._min = None61 other._n = 062 63 def find_min(self) -> int:64 assert self._min is not None65 return self._min.key66 67 def __len__(self) -> int:68 return self._n69 703 · Extract-min and consolidate71 def extract_min(self) -> int:72 z = self._min73 if z is None:74 raise IndexError("extract from empty heap")75 while z.child is not None: # promote every child to the root list76 c = z.child77 z.child = None if c.right is c else c.right78 self._unlink(c)79 c.parent = None80 c.mark = False81 self._splice_in(z, c)82 if z.right is z:83 self._min = None84 else:85 self._min = z.right86 self._unlink(z)87 self._consolidate()88 self._n -= 189 return z.key90 91 def _consolidate(self) -> None:92 deg: list[FibNode | None] = [None] * 64 # max degree is O(log n)93 roots = []94 cur = self._min95 while True:96 roots.append(cur)97 cur = cur.right98 if cur is self._min:99 break100 for x in roots:101 d = x.degree102 while deg[d] is not None: # equal degree: link larger under smaller103 y = deg[d]104 if y.key < x.key:105 x, y = y, x106 self._unlink(y)107 y.parent = x108 if x.child is None:109 x.child = y110 else:111 self._splice_in(x.child, y)112 x.degree += 1113 y.mark = False114 deg[d] = None115 d += 1116 deg[d] = x117 self._min = None # rebuild the root list from the degree table118 for r in deg:119 if r is None:120 continue121 r.left = r.right = r122 if self._min is None:123 self._min = r124 else:125 self._splice_in(self._min, r)126 if r.key < self._min.key:127 self._min = r128 1294 · Decrease-key and cascading cut130 def decrease_key(self, x: FibNode, new_key: int) -> None:131 if new_key > x.key:132 raise ValueError("new key is larger than current key")133 x.key = new_key134 p = x.parent135 if p is not None and x.key < p.key:136 self._cut(x, p)137 self._cascading_cut(p)138 if self._min is not None and x.key < self._min.key:139 self._min = x140 141 def _cut(self, x: FibNode, p: FibNode) -> None:142 if p.child is x:143 p.child = None if x.right is x else x.right144 self._unlink(x)145 p.degree -= 1146 x.parent = None147 x.mark = False148 self._splice_in(self._min, x)149 150 def _cascading_cut(self, p: FibNode) -> None:151 g = p.parent152 if g is None:153 return154 if not p.mark:155 p.mark = True # first lost child: mark156 else: # second lost child: cut p too157 self._cut(p, g)158 self._cascading_cut(g)FibNode.__slots__keeps the seven fields compact;left/rightstart asself, so a fresh node is a valid circular list.insert/mergeare O(1) root-list splices;mergedrains the other heap so no node is owned twice.extract_minpromotes children, unlinks the minimum, and_consolidatelinks equal-degree trees using a 64-slot degree table before rebuilding the root list.decrease_keyrequires theFibNodehandle returned byinsert;_cutmoves a violating node to the roots and_cascading_cutclimbs through marked ancestors.- Practical Python note: Dijkstra with
heapqplus lazy deletion beats this class on every realistic input — Fibonacci heaps matter for the theory.
Per-node Python object overhead dwarfs the binary heap’s flat list; expect large constant factors.
- Identity checks (
is,is not) are correct for the sentinel-free circular lists — nodes are compared by identity, keys by value. from __future__ import annotationsletsFibNode | Noneannotations evaluate lazily on older 3.x versions._cascading_cutis recursive; cut chains are short in practice, but an explicit loop avoids recursion-limit worries on adversarial inputs.
- Using
==whereisis needed (e.g.cur is self._minto terminate circular-list walks). - Forgetting
z.child = None if c.right is c else c.rightand looping forever on the last child. - Calling
decrease_keywith a key larger than the current one — the structure silently breaks without the guard.
- All four versions return a node handle from insert — the one API difference from library heaps, and the price of O(1) decrease-key. C++ exposes Node*, the others the node object.
- Memory management: C++ needs an explicit recursive destructor and deleted copies; JS/TS/Python let the GC collect the whole forest when the heap is dropped.
- Null handling: TS strict null checks force explicit empty-heap branches; Python uses is/is not identity; C++ uses nullptr; JS mixes null (empty) and undefined (return value).
- No mainstream standard library ships a Fibonacci heap; C++ has boost::heap::fibonacci_heap, the rest of the ecosystem uses binary/pairing heaps with lazy deletion instead.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(1) | O(1) | Minimum only. |
| Search | O(n) | O(n) | |
| Insert | O(1) | O(1) | Amortized and actual. |
| Delete | O(log n) | O(n) | Amortized O(log n); a single call may consolidate O(n) roots. |
| Update | O(1) | O(n) | Decrease-key amortized O(1); cascading cuts can be long. |
| Find-min | O(1) | O(1) | |
| Extract-min | O(log n) | O(n) | Amortized O(log n). |
| Decrease-key | O(1) | O(n) | Amortized O(1). |
| Merge | O(1) | O(1) | |
| Space | O(n) | Four pointers plus degree and mark per node. | |
Advantages & disadvantages
- Best known amortized bounds for a mergeable priority queue with decrease-key.
- Improves Dijkstra and Prim to
O(E + V log V)on dense graphs. - Merging is
O(1)— binary heaps needO(n).
- Large constant factors: four pointers per node, poor cache locality, and complex consolidation. Binary and pairing heaps are faster in practice.
- Amortized, not worst-case: a single extract-min can take
O(n)after many inserts, which is unacceptable for real-time systems. - Roughly 200 lines of delicate pointer code; almost never written in interviews or production.
- Not in standard libraries of Python, Java, C++, JavaScript, or Go.
Use cases
- Theoretical analysis of Dijkstra's Algorithm and Prim's Algorithm.
- Algorithms whose cost is dominated by decrease-key (some network-flow and matching algorithms).
- Teaching amortized analysis with a potential function.
- Proving asymptotic bounds where decrease-key dominates (dense-graph Dijkstra/Prim).
- Priority queues that must be merged in
O(1). - Explaining amortized analysis with a potential function.
- Anything performance-sensitive in practice — a Binary Heap (or a pairing heap) is faster on real hardware.
- Real-time systems that need worst-case guarantees; use a binary heap or a balanced BST.
- Interviews where you are asked to implement a priority queue — write a Binary Heap.
- Sparse graphs, where
E log Vis already close toE + V log V.
Alternatives
Common mistakes
- Believing the bounds are worst-case; they are amortized.
- Assuming Fibonacci-heap Dijkstra is faster in practice than binary-heap Dijkstra with lazy deletion — it usually is not.
- Forgetting to clear the mark when a node is cut to the root list.
- Not updating the parent's
childpointer when cutting the node it points to. - Consolidating with a degree table too small for
log_φ n.
Interview patterns
- Compare priority-queue implementations: binary, binomial, Fibonacci, pairing — bounds and trade-offs.
- Derive Dijkstra's
O(E + V log V)and explain why binary heaps are used anyway. - Explain the potential-function argument for amortized
O(1)decrease-key.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Where does O(n log n) come from?Beginner
- Top K from a streamIntermediate
- When a hash map is the wrong choiceIntermediate
- Kth Largest Element in an ArrayIntermediate
- Network Delay TimeAdvanced
- Top K Frequent ElementsIntermediate