Skip to the content
Software Made Clear Diagrams that show the mechanism About

VISUALISERS · FOUNDATIONS

← All visualisers

Dijkstra visualiser

Dijkstra answers why the cheapest node next, every time?

Every node in the figure is in one of three states: settled — its distance is final, and the badge on it says which turn it settled on; frontier — reached, with a distance that can still fall; not reached — no route to it yet, so its distance is still ∞.

Nothing settled yet. Only the source has a distance.

42158102635A0B∞C∞D∞E∞F∞G∞Settled 0 of 7Order: nothing yet42158102635A0B∞C∞D∞E∞F∞G∞Settled 0 of 7Order: nothing yet

SETTLED

0

NODES

7

“Step” advances one step, which is either settling the cheapest node on the frontier or relaxing one edge out of the node just settled; “Back” retraces one. “Run” plays out the steps that are left. Once all seven nodes are settled the run is over — “Step” and “Run” then have nothing left to do, and “Reset” returns to the start.

Dijkstra settles the cheapest node on the frontier every time, so the settle order never goes backwards in distance. Settling is final — a settled node is never looked at again — and that is why one negative edge breaks the answer: a cheaper route into an already-settled node would arrive too late to be taken.

What the visualiser is storing

Each successful relaxation changes two values: the cheapest distance found so far and the previous node on that route. The figure shows the distances; a real implementation also keeps the previous-node map so it can return a path, not just a cost.

In the worked graph, step to the end and read the distance beside each node. To reconstruct the route to a target, start there and follow previous nodes backwards until the source, then reverse that list.

Dijkstra in Python, including path reconstruction

This complete example uses an adjacency list and Python’s min-heap. It checks every edge before searching, so an early return cannot hide a negative weight. The arrival counter breaks equal-cost ties without comparing node labels; the distance check skips stale heap entries.

from heapq import heappop, heappush
from itertools import count
from math import inf, isfinite

def shortest_path(graph, source, target):
    graph = {node: list(edges) for node, edges in graph.items()}
    nodes = set(graph)
    for edges in graph.values():
        for neighbour, weight in edges:
            if not isfinite(weight) or weight < 0:
                raise ValueError("Dijkstra needs finite non-negative weights")
            nodes.add(neighbour)
    if source not in nodes or target not in nodes:
        raise ValueError("Source and target must be graph nodes")
    distance = {node: inf for node in nodes}
    previous = {}
    distance[source] = 0
    arrival = count()
    frontier = [(0, next(arrival), source)]

    while frontier:
        cost, _, node = heappop(frontier)
        if cost != distance[node]:
            continue  # A cheaper entry for this node won.
        if node == target:
            break

        for neighbour, weight in graph.get(node, []):
            candidate = cost + weight
            if candidate < distance[neighbour]:
                distance[neighbour] = candidate
                previous[neighbour] = node
                heappush(frontier, (candidate, next(arrival), neighbour))

    if distance[target] == inf:
        return inf, []

    path = []
    node = target
    while node != source:
        path.append(node)
        node = previous[node]
    path.append(source)
    return distance[target], list(reversed(path))

# Illustrative three-node graph: the cheaper route goes through C.
graph = {"A": [("B", 4), ("C", 2)], "C": [("B", 1)]}
print(shortest_path(graph, "A", "B"))
# (3, ['A', 'C', 'B'])

Use hashable node labels and finite non-negative numeric weights whose path sums remain finite. A node appearing only as an edge target has no outgoing edges. Unknown source or target nodes are rejected; an unreachable known target returns infinity and an empty path. This illustrative implementation is not a benchmark or a graph-library replacement.

Why the data structure changes the running time

The shortest-path rule stays the same. What changes is how much work it takes to choose the cheapest frontier node.

Frontier choiceTimeTrade-off
Array or linear scanO(V²)Simple, and often reasonable for dense graphs where the edge count is close to V².
Adjacency list plus binary heapO((V + E) log V)Usually the practical choice for sparse graphs because it avoids scanning every node for each settlement.

V counts vertices and E counts edges. The table assumes a simple graph. The lazy heap in the published code can hold O(E) entries, including stale ones: its general bound is O(V + E log(E + 1)) time, which gives the table’s bound for simple graphs. Distance and previous-node maps use O(V) space; the copied adjacency list and heap add O(V + E) space.

Choose the algorithm before the optimisation

Use breadth-first search when every edge has the same cost. Use Dijkstra when weights are non-negative. If a negative edge is possible, use an algorithm designed for it, such as Bellman–Ford; changing the priority queue cannot repair Dijkstra’s settled-node assumption.

The mechanism and complexity bounds were checked against the MIT 6.006 single-source shortest-path notes; the heap operations use Python’s heapq interface. The example and explanation here are original. MIT 6.006 lecture 16 · Python heapq documentation

Want an explanation to go with the picture? Find a guided starting point →