Data Structure 📂 Graph Algorithms · 2 of 6 37 min read

Dijkstra's Algorithm, Hands-On: Shortest Paths

An interactive, beginner-friendly Dijkstra tutorial in Python where the code and an animated graph run together — nodes settle, edges relax, distance labels and the priority queue update live, and the executing line is highlighted at each step. Includes the GPS/ripple analogy, the five-step process, the full step-by-step working table, path reconstruction, why weights must be non-negative, complexity, benefits, use cases, and a comparison with BFS, Bellman-Ford, A*, and Floyd-Warshall.

Section 01

The Story That Explains Dijkstra

Your GPS Finding the Fastest Route
You type a destination into a map app and, in a blink, it returns the quickest route through a web of thousands of roads. How? It doesn't try every possible route — that would take forever. Instead it explores outward from your location like a ripple in a pond: it always expands the closest unexplored junction first, locking in each junction's shortest travel time before moving to the next-closest.

Because it always settles the nearest point first — and roads can't have negative travel time — once a junction is settled, no later detour could ever beat it. That ripple-outward, always-take-the-cheapest strategy is exactly Dijkstra's algorithm, designed by Edsger Dijkstra in 1956 and still powering GPS, the internet, and games today.

Dijkstra's algorithm finds the shortest path from one source node to every other node in a weighted graph. It is greedy: at each step it picks the unvisited node with the smallest known distance, "settles" it as final, and uses it to improve its neighbours. The one rule: all edge weights must be non-negative.

💧
The Core Move: Relaxation

For an edge u → v with weight w, ask: "is my path to u plus w shorter than the best I currently know for v?" If yes, relax it — update v's distance. Dijkstra is just relaxation, applied in the smartest possible order.

SRC

Dijkstra explores outward by distance — nearest nodes settle first, like ripples spreading.


Section 02

The Five Steps

01
Initialise Distances
Set the source's distance to 0 and every other node to (unknown / unreachable so far).
02
Pick the Closest Unvisited Node
Use a min-priority queue (a heap) to pop the node with the smallest tentative distance. The source comes out first.
03
Settle It
Mark that node visited. Because weights are non-negative, its distance is now final — nothing can improve it later.
04
Relax Its Neighbours
For each neighbour, if going through this node is cheaper, update the neighbour's distance and push it onto the queue.
05
Repeat Until the Queue Is Empty
Keep popping and relaxing. When the queue empties, every node holds its true shortest distance from the source.

Section 03

The Graph We'll Use

Six nodes (A–F), source A, with non-negative edge weights. Here is its adjacency list — each node and the neighbours it connects to, with the cost of each edge.

NodeConnected to (weight)
A (source)B (4), C (2)
BA (4), C (1), D (5)
CA (2), B (1), D (8), E (10)
DB (5), C (8), E (2), F (6)
EC (10), D (2), F (3)
FD (6), E (3)

Section 04

Hands-On: Watch Each Line Run

The heart of the tutorial. Press Play (or Step) and watch the graph come alive: nodes light up as they're picked and settled, edges flash amber while being relaxed and turn green when they join the shortest-path tree, and the distance labels update live. The priority queue and distance table track state, while the exact Python line running lights up on the right.

📍 Live Dijkstra — code ↔ animated graph source: A
current in queue settled unvisited
Distances from A
Priority queue (min-heap)
Press Play or Step ▶ to begin. The highlighted line on the right is the one running.
0Settled
0Queue size
0Relaxations
import heapq
def dijkstra(graph, src):
    dist = {v: float('inf') for v in graph}
    dist[src] = 0
    pq = [(0, src)]
    visited = set()
    while pq:
        d, u = heapq.heappop(pq)
        if u in visited:
            continue
        visited.add(u)
        for v, w in graph[u]:
            if d + w < dist[v]:
                dist[v] = d + w
                heapq.heappush(pq, (dist[v], v))
    return dist
💡
What the colours mean

Amber node = currently being processed. Green node = settled (final distance). Indigo node = discovered, waiting in the queue. Amber edge = being relaxed right now; green edge = part of the shortest-path tree.


Section 05

The Code, Line by Line — "This Line Does That"

Same program as the animation, fully annotated. Each line maps to a job the visualiser highlights.

import heapq

def dijkstra(graph, src):
    dist = {v: float('inf') for v in graph}  # every node unknown
    dist[src] = 0                          # source is 0 away from itself
    pq = [(0, src)]                       # min-heap of (distance, node)
    visited = set()                        # settled nodes
    while pq:                              # until nothing left to explore
        d, u = heapq.heappop(pq)           # closest unsettled node
        if u in visited:                    # stale duplicate?
            continue                      # skip it
        visited.add(u)                      # settle u - distance is final
        for v, w in graph[u]:               # look at each neighbour
            if d + w < dist[v]:            # cheaper path found?
                dist[v] = d + w           # relax: update distance
                heapq.heappush(pq, (dist[v], v))  # queue it for later
    return dist                           # shortest distance to every node
🔎 What Each Line Is Responsible For
Line 1
import heapq — Python's binary-heap module gives us an efficient min-priority queue.
Line 3
dist = {v: float('inf') ...}Step 1: every distance starts at ∞ (unknown).
Line 4
dist[src] = 0 — the source is zero distance from itself.
Line 5
pq = [(0, src)] — seed the queue with the source. Tuples sort by distance first.
Line 6
visited = set() — tracks which nodes are already settled.
Line 7
while pq — keep going as long as the queue has nodes to explore.
Line 8
d, u = heappop(pq)Step 2: pop the closest unsettled node in O(log V).
Line 9–10
if u in visited: continue — the heap can hold stale duplicates; skip any node already settled.
Line 11
visited.add(u)Step 3: settle u. Its distance is now final.
Line 12
for v, w in graph[u]Step 4: iterate over u's neighbours and their edge weights.
Line 13
if d + w < dist[v] — the relaxation test: is the path through u cheaper?
Line 14
dist[v] = d + w — relax the edge: record the better distance.
Line 15
heappush(pq, (dist[v], v)) — queue v with its new distance so it gets explored later.
Line 16
return dist — the shortest distance from the source to every node.
OUTPUT — dijkstra(graph, 'A')
{'A': 0, 'B': 3, 'C': 2, 'D': 8, 'E': 10, 'F': 13}

Section 06

Full Working — Step by Step

Here is the complete trace from source A. Each row is one node being settled, the distances it improves, and the queue afterwards. (stale) entries are old queue items that get skipped when popped.

#SettleRelaxationsDistances afterQueue after
initA = 0A0, B∞, C∞, D∞, E∞, F∞(0,A)
1A (0)B→4, C→2A0, B4, C2, D∞, E∞, F∞(2,C),(4,B)
2C (2)B 4→3, D→10, E→12A0, B3, C2, D10, E12, F∞(3,B),(4,B),(10,D),(12,E)
3B (3)D 10→8A0, B3, C2, D8, E12, F∞(4,B)*,(8,D),(10,D),(12,E)
4D (8)E 12→10, F→14A0, B3, C2, D8, E10, F14(10,D)*,(10,E),(12,E)*,(14,F)
5E (10)F 14→13A0, B3, C2, D8, E10, F13(12,E)*,(13,F),(14,F)*
6F (13)noneA0, B3, C2, D8, E10, F13(14,F)*
🎉
Final Shortest Paths from A

Notice how B improved 4→3, D improved 10→8, E improved 12→10, and F improved 14→13 as better paths were discovered — that is relaxation in action.

DestinationShortest DistancePath
A0A
B3A → C → B
C2A → C
D8A → C → B → D
E10A → C → B → D → E
F13A → C → B → D → E → F

Section 07

Reconstructing the Actual Path

The version above returns distances. To get the route, also record each node's predecessor, then walk it backwards from the target.

import heapq

def dijkstra_path(graph, src, target):
    dist = {v: float('inf') for v in graph}
    prev = {v: None for v in graph}   # who we came from
    dist[src] = 0
    pq = [(0, src)]
    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]:
            continue
        for v, w in graph[u]:
            if d + w < dist[v]:
                dist[v] = d + w
                prev[v] = u            # remember the step
                heapq.heappush(pq, (dist[v], v))
    # walk predecessors backwards
    path, node = [], target
    while node is not None:
        path.append(node)
        node = prev[node]
    return dist[target], path[::-1]
OUTPUT — dijkstra_path(graph, 'A', 'F')
(13, ['A', 'C', 'B', 'D', 'E', 'F'])

Section 08

Why Non-Negative Weights?

⚠️
The One Rule You Can't Break

Dijkstra settles a node and never revisits it, trusting that no future path can beat the one it just locked in. A negative edge shatters that trust: a cheaper route could appear after the node is settled, but Dijkstra has already moved on — producing a wrong answer. For graphs with negative weights, use Bellman-Ford instead.

✅ Non-negative weights
PropertyResult
Settle onceAlways correct
Greedy choiceProvably optimal
UseDijkstra ✓
❌ Negative weights
PropertyResult
Settle onceMay be wrong
Greedy choiceBreaks down
UseBellman-Ford

Section 09

Time & Space Complexity

ImplementationTimeSpaceBest For
Binary heap (heapq)O((V + E) log V)O(V + E)Sparse graphs (most real graphs)
Simple array scanO(V²)O(V)Dense graphs (E ≈ V²)
Fibonacci heapO(E + V log V)O(V + E)Theoretical best; rarely used
🔢
Reading the Heap Version

V = nodes, E = edges. Each node is popped once (V pops) and each edge can trigger a push (E pushes), and every heap operation costs O(log V) — giving O((V + E) log V). This is why the binary-heap version is the everyday default.


Section 10

Benefits

🎯
Provably Optimal
non-negative weights
It guarantees the true shortest path from the source to every node — not an approximation.
Efficient on Real Graphs
O((V+E) log V)
With a binary heap it scales comfortably to large, sparse networks like road maps and the internet.
🌍
Single-Source, All Targets
one run, every node
One execution gives the shortest distance from the source to every reachable node at once.
🧮
Works on Directed & Undirected
flexible
Handles one-way edges, two-way edges, and varying weights with no change to the core logic.
🔗
Easy Path Recovery
predecessor map
Track a predecessor per node and you can reconstruct the actual route, not just its length.
🧠
Foundation for A*
extensible
Add a heuristic and Dijkstra becomes A* — the basis of fast game and map pathfinding.

Section 11

When To Use It — and When Not To

GPS & Map Routing
Road networks have non-negative travel times. Dijkstra (and its A* extension) is the classic engine behind turn-by-turn directions.
maps, logistics, delivery
Network Routing
Protocols like OSPF use Dijkstra to compute least-cost paths for routing packets across the internet.
OSPF, IS-IS, telecom
Games & Robotics
Pathfinding for units, NPCs, and robots over weighted grids or waypoint graphs — often via A*, which builds on Dijkstra.
pathfinding, navigation
Negative Edge Weights
Dijkstra can return wrong answers with negative edges. Use Bellman-Ford (and detect negative cycles) instead.
use Bellman-Ford
All-Pairs Shortest Paths
Need every pair's distance on a dense graph? Floyd-Warshall is simpler than running Dijkstra from every node.
use Floyd-Warshall
Unweighted Graphs
If all edges cost the same, a plain BFS finds shortest paths faster and with far less overhead.
use BFS

Section 12

Dijkstra vs Other Path Algorithms

AlgorithmSolvesWeightsTimeNote
BFSSingle-sourceUnweightedO(V + E)Fastest when all edges equal
DijkstraSingle-sourceNon-negativeO((V+E) log V)The go-to weighted sorter
Bellman-FordSingle-sourceAny (no neg cycle)O(V·E)Handles negative edges
A*Single-pairNon-negativeHeuristic-dependentDijkstra + a heuristic
Floyd-WarshallAll-pairsAny (no neg cycle)O(V³)Every pair at once
🧩
A Quick Decision Guide

Unweighted? BFS. Weighted, non-negative, one source? Dijkstra. Got negative edges? Bellman-Ford. Just one start–goal pair with a good distance estimate? A*. Need every pair on a small dense graph? Floyd-Warshall.


Section 13

Golden Rules

📍 Dijkstra — Things to Remember
1
Non-negative weights only. The whole correctness proof rests on this. Negative edge? Switch to Bellman-Ford.
2
Always use a min-heap (heapq) for the priority queue. A linear scan makes it O(V²) — fine for dense graphs, slow for sparse ones.
3
The heap holds stale duplicates. Guard with if u in visited: continue (or if d > dist[u]: continue) so you skip outdated entries.
4
Once a node is popped and settled, its distance is final. Never reprocess it. That's what makes the algorithm efficient.
5
Want the route, not just the cost? Store a prev predecessor for every relaxed node and walk it backwards from the target.
6
Remember the picture: a ripple expanding outward, always settling the nearest unsettled node first. If you can see that, you understand Dijkstra.