#algorithms #python #graphs #dsa #competitive-programming

How Dijkstra's Algorithm Actually Works — Python Implementation with a Min-Heap

· 9 min read

The Problem Nobody Explains Properly

Every DSA tutorial shows you the naive O(V²) Dijkstra. Interviewers want the O((V + E) log V) heap version. If you can’t explain why the heap makes it faster, you’re going to lose points.

This article walks through:

  1. Why the naive version is too slow
  2. How a min-heap fixes it
  3. A clean Python implementation you can actually memorize
  4. The one edge case that trips everyone up

Why the Naive Version Is O(V²)

The naive implementation tracks the minimum distance vertex by scanning all unvisited vertices at each step:

# O(V) scan at every step → O(V²) total
min_vertex = min(unvisited, key=lambda v: dist[v])

For a dense graph (E ≈ V²) this is fine. For sparse graphs — which is most real-world graphs — it’s catastrophic.


How a Min-Heap Reduces It to O((V + E) log V)

A min-heap gives you the minimum-distance vertex in O(log V) instead of O(V). Since you push at most E times and pop at most V times:

  • Push operations: O(E log V)
  • Pop operations: O(V log V)
  • Total: O((V + E) log V)

For a sparse graph where E ≈ V, this is O(V log V) vs O(V²) — a massive improvement.


The Python Implementation

import heapq
from collections import defaultdict

def dijkstra(graph: dict[int, list[tuple[int, int]]], src: int, n: int) -> list[int]:
    """
    graph: adjacency list — graph[u] = [(v, weight), ...]
    src:   source vertex (0-indexed)
    n:     total number of vertices
    
    Returns: dist[] where dist[i] = shortest distance from src to i,
             or float('inf') if unreachable.
    """
    dist = [float('inf')] * n
    dist[src] = 0

    # Min-heap: (distance, vertex)
    heap = [(0, src)]

    while heap:
        d, u = heapq.heappop(heap)

        # CRITICAL: skip stale entries — this is the #1 bug in student code
        if d > dist[u]:
            continue

        for v, weight in graph[u]:
            new_dist = dist[u] + weight
            if new_dist < dist[v]:
                dist[v] = new_dist
                heapq.heappush(heap, (new_dist, v))

    return dist

Why if d > dist[u]: continue?

Python’s heapq has no decrease-key operation. Instead of updating an existing heap entry, you push a new entry with the better distance. The old (stale) entry stays in the heap.

When you pop a stale entry (old_dist, u), old_dist > dist[u] is true — you’ve already processed u with a better path. Skipping it is correct and free.

Without this check, you’d process u multiple times and push its neighbors again with redundant distances. The algorithm still produces correct results (eventually), but it degrades to O(E log E) worst case.


Quick Test

# Build a weighted directed graph
graph = defaultdict(list)
edges = [(0, 1, 4), (0, 2, 1), (2, 1, 2), (1, 3, 1), (2, 3, 5)]
for u, v, w in edges:
    graph[u].append((v, w))

dist = dijkstra(graph, src=0, n=4)
print(dist)  # [0, 3, 1, 4]
# 0→2 (cost 1) → 1 (cost 2) = 3  ✓
# 0→2 (cost 1) → 1 (cost 2) → 3 (cost 1) = 4  ✓

Common Interview Pitfalls

MistakeFix
Forgetting the stale-entry checkAlways add if d > dist[u]: continue
Using dist[src] = 0 but never pushing (0, src) to heapInitialize heap with source
Assuming undirected graph — adding only one directionAdd both u→v and v→u for undirected
Negative weightsDijkstra does not work with negative weights. Use Bellman-Ford instead
Heap contains vertex objects instead of indicesKeep heap entries as (int, int) — Python compares tuples lexicographically

Time & Space Complexity

Complexity
TimeO((V + E) log V)
SpaceO(V + E) for graph + O(V) for dist + O(E) for heap

The heap can contain at most E entries (one per edge push), giving O(E) heap space.


What to Say in an Interview

“I’m using a min-heap to always process the closest unvisited vertex. Since Python’s heapq doesn’t support decrease-key, I allow duplicate entries and skip stale ones. Total time complexity is O((V + E) log V) which is optimal for sparse graphs.”

That answer covers correctness, the heap choice rationale, the stale-entry trick, and complexity. That’s a complete answer.

Found this useful? Explore real student projects on CodexCampus.

Explore the Vault →