Dijkstra's Shortest Path Algorithm Explained with Examples
If you've ever used Google Maps or wondered how network packets find the fastest route, you've already seen Dijkstra's algorithm in action. It's one of those algorithms that sounds intimidating but is surprisingly straightforward once you see it step by step.
So let's break it down.
Interactive visualization
Use the controls below to step through the algorithm or let it play automatically.
name: dijkstra
input: {"nodes": ["A", "B", "C", "D", "E"], "edges": [["A", "B", 4], ["A", "C", 2], ["B", "D", 3], ["B", "C", 1], ["C", "D", 4], ["C", "E", 5], ["D", "E", 1]], "start": "A"}
speed: 700
What does Dijkstra's algorithm actually do?
Given a graph with weighted edges and a starting node, Dijkstra's algorithm finds the shortest path from that starting node to every other node. The weights represent costs, like distance, time, or bandwidth.
Here's the thing: it only works with non-negative edge weights. If you've got negative weights, you'll need Bellman-Ford instead. But for most real-world problems, non-negative weights are what you're dealing with.
The core idea
The algorithm is greedy. It always picks the unvisited node with the smallest known distance, locks it in, and then checks if going through that node gives a shorter path to its neighbors. That's it. Repeat until you've visited everything.
Think of it like exploring a city. You start at home, check which nearby intersection is closest, walk there, then look around from that new spot. You never backtrack to reconsider a place you've already committed to.
Visualizing the graph
Let's work through a concrete example. Here's our weighted graph:
We want to find the shortest path from A to every other node.
Step-by-step walkthrough
We'll track distances in a table and use a priority queue to always process the closest unvisited node first.
Initial state
| Node | Distance | Visited | Previous |
|---|---|---|---|
| A | 0 | No | - |
| B | inf | No | - |
| C | inf | No | - |
| D | inf | No | - |
| E | inf | No | - |
Priority queue: [(0, A)]
Step 1: Process A (distance 0)
We look at A's neighbors. B is 4 away, C is 2 away, E is 7 away. All of these are less than infinity, so we update them.
| Node | Distance | Visited | Previous |
|---|---|---|---|
| A | 0 | Yes | - |
| B | 4 | No | A |
| C | 2 | No | A |
| D | inf | No | - |
| E | 7 | No | A |
Priority queue: [(2, C), (4, B), (7, E)]
Step 2: Process C (distance 2)
C is closest. From C, we can reach B (2+1=3, which beats 4), D (2+5=7), and E (2+8=10, worse than 7).
| Node | Distance | Visited | Previous |
|---|---|---|---|
| A | 0 | Yes | - |
| B | 3 | No | C |
| C | 2 | Yes | A |
| D | 7 | No | C |
| E | 7 | No | A |
Notice B's distance dropped from 4 to 3. We found a better route through C.
Priority queue: [(3, B), (7, D), (7, E)]
Step 3: Process B (distance 3)
From B, we can reach D (3+3=6, which beats 7).
| Node | Distance | Visited | Previous |
|---|---|---|---|
| A | 0 | Yes | - |
| B | 3 | Yes | C |
| C | 2 | Yes | A |
| D | 6 | No | B |
| E | 7 | No | A |
Priority queue: [(6, D), (7, E)]
Step 4: Process D (distance 6)
From D, we can reach E (6+2=8, worse than 7). No updates.
| Node | Distance | Visited | Previous |
|---|---|---|---|
| A | 0 | Yes | - |
| B | 3 | Yes | C |
| C | 2 | Yes | A |
| D | 6 | Yes | B |
| E | 7 | No | A |
Step 5: Process E (distance 7)
E has no unvisited neighbors. We're done.
The shortest path tree
Here's what the final shortest paths look like:
The shortest path from A to D, for example, is A -> C -> B -> D with a total cost of 6. Not A -> B -> D (which would be 7).
Python implementation
Here's a clean implementation using Python's heapq for the priority queue:
import heapq
from collections import defaultdict
def dijkstra(graph, start):
# Distance to every node starts at infinity
distances = {node: float('inf') for node in graph}
distances[start] = 0
previous = {node: None for node in graph}
# Priority queue: (distance, node)
pq = [(0, start)]
visited = set()
while pq:
current_dist, current_node = heapq.heappop(pq)
# Skip if we already found a better path
if current_node in visited:
continue
visited.add(current_node)
for neighbor, weight in graph[current_node]:
if neighbor in visited:
continue
new_dist = current_dist + weight
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
previous[neighbor] = current_node
heapq.heappush(pq, (new_dist, neighbor))
return distances, previous
def get_path(previous, start, end):
path = []
current = end
while current is not None:
path.append(current)
current = previous[current]
path.reverse()
if path[0] != start:
return [] # No path exists
return path
# Build the graph from our example
graph = defaultdict(list)
edges = [
('A', 'B', 4), ('A', 'C', 2), ('A', 'E', 7),
('B', 'D', 3), ('B', 'C', 1),
('C', 'D', 5), ('C', 'E', 8),
('D', 'E', 2),
]
for u, v, w in edges:
graph[u].append((v, w))
graph[v].append((u, w)) # Undirected graph
distances, previous = dijkstra(graph, 'A')
print("Shortest distances from A:")
for node, dist in sorted(distances.items()):
path = get_path(previous, 'A', node)
print(f" {node}: {dist} (path: {' -> '.join(path)})")
Output:
Shortest distances from A:
A: 0 (path: A)
B: 3 (path: A -> C -> B)
C: 2 (path: A -> C)
D: 6 (path: A -> C -> B -> D)
E: 7 (path: A -> E)
How the priority queue drives everything
The priority queue is doing the heavy lifting here. By always processing the closest unvisited node, we guarantee that once we visit a node, we've already found the shortest path to it. That's the greedy property that makes this work.
Time and space complexity
Time complexity: O((V + E) log V) when using a binary heap priority queue. Every node gets pushed and popped from the heap (O(V log V)), and every edge triggers a potential push (O(E log V)).
Space complexity: O(V + E) for storing the graph, distances, and the priority queue.
If you use a Fibonacci heap, you can get O(V log V + E), but honestly nobody does that in practice. The binary heap version is fast enough for nearly everything.
When to use Dijkstra's algorithm
GPS navigation. Finding the fastest route between two locations. Roads are edges, intersections are nodes, and travel times are weights. Every navigation app uses some variant of this.
Network routing. Protocols like OSPF use Dijkstra's algorithm to find the shortest path for data packets. Each router builds a map of the network and runs the algorithm to determine the best next hop.
Game pathfinding. When an NPC needs to navigate a map with varying terrain costs (roads are cheap, swamps are slow), Dijkstra's handles it naturally. For large maps, A* is usually preferred since it adds a heuristic, but Dijkstra's is the foundation A* builds on.
Social networks. Finding the shortest connection between two people. The weights could represent how strong a connection is.
Common pitfalls
Don't use Dijkstra's with negative edge weights. It assumes that once a node is visited, its distance is final. Negative edges break that assumption because a longer path with a negative edge could end up shorter.
Also, watch out for disconnected graphs. If a node is unreachable from your source, its distance stays at infinity. Your code should handle that gracefully.
One more thing: if all edge weights are equal (unweighted graph), you don't need Dijkstra's at all. Plain BFS will give you shortest paths and it's simpler and faster.
Wrapping up
Dijkstra's algorithm is one of those things that clicks once you see the priority queue pattern. You pick the closest unvisited node, update its neighbors, repeat. The priority queue makes sure you're always making the optimal greedy choice.
Now that you understand how it works, try implementing it yourself with a different graph. Change the weights around, add more nodes, see how the shortest paths shift. That's the best way to really internalize it.