Implementing Dijkstra’s Algorithm in Python with heapq: Complexity, Edge‑Weight Rules, and Practical Trade‑offs
Implement Dijkstra’s algorithm in Python using heapq: understand the O((V+E) log V) complexity, handle non‑negative edge weights, and learn practical trade‑offs for sparse graphs. Includes a worked example and testing tips.
26 Aug 2025, 21:56 UTC

Why Dijkstra and heapq?
When you need the shortest path between a source node and every other node in a weighted graph, Dijkstra’s algorithm is the go‑to solution. It guarantees optimality as long as all edge weights are non‑negative. In Python, the heapq module offers a ready‑made binary min‑heap that can serve as the priority queue required by the algorithm. The combination is fast, memory‑efficient for sparse graphs, and easy to understand.
Complexity in a Nutshell
The algorithm performs a push and pop on the heap for each edge that is relaxed. Each heap operation costs O(log V), where V is the number of vertices. Thus the overall time complexity is:
O((V + E) · log V)
For sparse graphs where the number of edges E is on the order of V, this scales roughly as O(E log V), which is very efficient. In contrast, a naive implementation that scans all vertices for the minimum distance would cost O(V^2) and quickly become impractical for thousands of nodes.
Edge‑Weight Constraints
Dijkstra’s algorithm only works correctly when every edge weight is non‑negative. If any weight is negative, the algorithm can produce sub‑optimal paths because it may prematurely finalize a vertex’s distance before discovering a cheaper route that uses the negative edge. In such cases, the Bellman‑Ford algorithm should be used instead.
Choosing the Right Graph Representation
For medium‑to‑large sparse graphs, an adjacency list is the most memory‑efficient choice. Each vertex stores a list of tuples (neighbor, weight). This format keeps memory usage proportional to E rather than V^2 as an adjacency matrix would. Using integer indices for vertices (e.g., 0–V-1) also speeds up lookups and reduces overhead.
heapq: No Decrease‑Key, But It’s Fine
The standard heapq library does not expose a decrease_key operation. A common workaround is to push a new (distance, node) pair onto the heap whenever a better distance is found. The algorithm then lazily discards stale entries when they pop. To keep the algorithm efficient, a dictionary best records the current best known distance for each vertex. When popping from the heap, the pair is compared against best; if it is outdated, the entry is skipped.
Concrete Worked Example
Consider the following weighted graph with five vertices (0–4):
| Edge | Weight |
|---|---|
| 0 – 1 | 4 |
| 0 – 2 | 1 |
| 2 – 1 | 2 |
| 1 – 3 | 5 |
| 2 – 3 | 8 |
| 3 – 4 | 3 |
The expected shortest‑path distances from source vertex 0 are:
- 0 → 0: 0
- 0 → 1: 3 (0–2–1)
- 0 → 2: 1
- 0 → 3: 8 (0–2–1–3)
- 0 → 4: 11 (0–2–1–3–4)
Below is a minimal, self‑contained implementation that follows the pattern described above. Run it in a Python 3.10+ interpreter; no external packages are required.
import heapq
def dijkstra(adj, start):
"""Return dict of shortest distances from start using heapq.
adj: dict[int, list[tuple[int, float]]] – adjacency list.
start: int – source vertex.
"""
dist = {v: float('inf') for v in adj}
dist[start] = 0
best = {start: 0}
heap = [(0, start)] # (distance, vertex)
while heap:
d, u = heapq.heappop(heap)
# Skip stale entries
if d != best.get(u, float('inf')):
continue
for v, w in adj[u]:
nd = d + w
if nd < dist[v]:
dist[v] = nd
best[v] = nd
heapq.heappush(heap, (nd, v))
return dist
# Example graph
adjacency = {
0: [(1, 4), (2, 1)],
1: [(0, 4), (2, 2), (3, 5)],
2: [(0, 1), (1, 2), (3, 8)],
3: [(1, 5), (2, 8), (4, 3)],
4: [(3, 3)]
}
print(dijkstra(adjacency, 0))
When executed, the output should resemble:
{0: 0, 1: 3, 2: 1, 3: 8, 4: 11}
Verify correctness by comparing the returned dictionary to the expected distances shown above.
Testing and Profiling
- Unit tests: Use
unittestorpytestto assert thatdijkstrareturns the correct distances for a variety of graphs, including isolated nodes and disconnected components. - Performance check: Create a synthetic sparse graph with 10,000 vertices and 50,000 edges. Measure runtime with
timeitorcProfile. The time should grow roughly in proportion to(V+E) log V. - Heap integrity: After each
heappushorheappop, you can callheapq.nsmallest(1, heap)to confirm the smallest element is indeed the minimal distance.
Trade‑offs and Limitations
- Negative weights: Dijkstra fails. Use Bellman‑Ford if negative edges are present.
- Dense graphs: If
E ≈ V^2, the heap operations dominate and memory usage of the adjacency list can become large. In such cases, an adjacency matrix or specialized libraries (e.g.,networkx) may be more appropriate. - Recursive path reconstruction: If you need to backtrack from a target node to the source, avoid a recursive approach on very deep graphs; the Python recursion limit (default 1000) can be hit. An iterative stack is safer.
Actionable Takeaways
- For medium‑to‑large sparse graphs with non‑negative weights, use
heapqand an adjacency list. The binary heap’sO((V+E) log V)performance is usually the sweet spot. - When you need a decrease‑key operation and can afford a more complex implementation, consider a Fibonacci heap library or an augmented
heapqwrapper. The constant factors, however, often outweigh the theoretical benefit for typical graph sizes. - Always validate your graph data: ensure weights are non‑negative and that the adjacency list is symmetrical for undirected graphs.
- For unit testing, include edge cases such as isolated nodes, disconnected components, and cycles with zero weight to guarantee robustness.
By following these guidelines, you can confidently implement Dijkstra’s algorithm in Python for real‑world applications ranging from network routing to game AI pathfinding.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.