Dijkstra

Dijkstra's algorithm finds shortest paths from a source in a graph with non-negative edge weights. O((V+E) log V) with a binary heap — the standard tool for routing, navigation, and network protocols (OSPF, IS-IS).

Edsger Dijkstra published the algorithm in 1959. It is a greedy algorithm: at every step it commits to the unvisited node with the smallest known distance, and the non-negative-weight assumption guarantees that no later path can improve on that commitment. The same idea underlies A* search, which adds a heuristic estimate of the remaining distance to steer the greedy choice toward a goal.

The main loop

One iteration of Dijkstra with a binary heap Pop min (d, u) from heap Stale? if d > dist[u]: skip entry Relax edges nd = d + w(u, v) for each neighbour v Improve if nd < dist[v]: dist[v] = nd, push Repeat until the heap is empty; each node is finalized the first time a non-stale entry for it is popped.

Python's heapq has no decrease-key operation, so the implementation pushes a new entry whenever a distance improves and leaves the old one in the heap. Old entries are recognized later because their distance is larger than the current dist[u], and they are discarded. This "lazy deletion" keeps the code short; the heap can hold up to E entries instead of V, which changes the log factor from log V to log E, and since E is at most V squared, that is still O(log V).

Step-by-step trace

dist[] after each pop on the example graph (source A) Columns A B C D Init: dist[A] = 0 0 inf inf inf Pop A (0): relax A-B 1, A-C 4 0 1 4 inf Pop B (1): relax B-C 2, B-D 5 0 1 3 6 Pop C (3): relax C-D 1 0 1 3 4 Pop D (4): no edges; stale (4, C) skipped 0 1 3 4 Yellow cells changed in that step; green row is final. C improves from 4 to 3 via B, and D from 6 to 4 via C.

The graph is the one in the code below: A to B costs 1, A to C costs 4, B to C costs 2, B to D costs 5, C to D costs 1. When B is popped, the path A-B-C (cost 3) beats the direct edge A-C (cost 4), so C is updated and pushed again; the old (4, C) entry stays in the heap and is skipped when it surfaces. The same happens for D, finally reached at cost 4 through A-B-C-D. Note that each node is finalized in increasing order of distance: A at 0, B at 1, C at 3, D at 4.

Complexity

Best Average Worst Space (Worst)
Ω((V+E) log V) Θ((V+E) log V) O((V+E) log V) O(V)

How it works

Maintain dist[v] initialized to ∞ except dist[source] = 0, and a min-heap keyed by tentative distance. Repeatedly pop the closest unvisited node; relax each outgoing edge (v, w) — if dist[v] + weight < dist[w], update and push w. Once a node is popped, its distance is final (relies on edges being non-negative).

Python implementation

import heapq


def dijkstra(graph, source):
    """graph[u] = list of (v, weight). Returns dict of shortest distances."""
    dist = {n: float("inf") for n in graph}
    dist[source] = 0
    pq = [(0, source)]
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue                          # stale entry
        for v, w in graph[u]:
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(pq, (nd, v))
    return dist


graph = {
    "A": [("B", 1), ("C", 4)],
    "B": [("C", 2), ("D", 5)],
    "C": [("D", 1)],
    "D": [],
}
print(dijkstra(graph, "A"))   # {'A': 0, 'B': 1, 'C': 3, 'D': 4}

Trade-offs

Recovering the actual path

Distances alone rarely suffice for routing; you need the route. Record the predecessor whenever an edge improves a distance, then walk predecessors back from the target. Stopping as soon as the target is popped is a valid optimization for single-pair queries, because its distance is final at that moment.

import heapq

def shortest_path(graph, source, target):
    dist = {source: 0}
    prev = {}
    pq = [(0, source)]
    while pq:
        d, u = heapq.heappop(pq)
        if u == target:
            break                                  # target is final; stop early
        if d > dist.get(u, float("inf")):
            continue
        for v, w in graph[u]:
            nd = d + w
            if nd < dist.get(v, float("inf")):
                dist[v], prev[v] = nd, u
                heapq.heappush(pq, (nd, v))
    if target not in dist:
        return None, []
    path, node = [], target
    while node != source:
        path.append(node)
        node = prev[node]
    path.append(source)
    return dist[target], path[::-1]


graph = {"A": [("B", 1), ("C", 4)], "B": [("C", 2), ("D", 5)], "C": [("D", 1)], "D": []}
print(shortest_path(graph, "A", "D"))   # (4, ['A', 'B', 'C', 'D'])

When to use it. Dijkstra is the default for single-source shortest paths on weighted graphs with non-negative weights: road networks, network routing (OSPF and IS-IS compute shortest-path trees this way), game maps, and dependency costs. For unweighted graphs, plain BFS gives the same answer in O(V + E). For a single target with a good distance estimate, A* explores far fewer nodes. With negative edges, use Bellman-Ford, or Johnson's algorithm for all pairs.

Pitfalls

← Back to Algorithms