Dijkstra’s Algorithm Explained: Step-by-Step with Examples
The complete guide to Dijkstra’s single-source shortest path algorithm — pseudocode, detailed trace, Python code, time complexity proof, and GATE-level problems.
Last updated: April 18, 2026 | Topic: Algorithms — Graph | Level: B.Tech / GATE CS
Key Takeaways
- Dijkstra finds the shortest path from one source to all other vertices in a weighted graph.
- Works correctly only on non-negative edge weights. Fails on negative weights.
- Uses a greedy approach with a priority queue (min-heap).
- Time complexity: O((V + E) log V) with a binary heap; O(V²) with simple array.
- Space complexity: O(V) for the distance and predecessor arrays.
- For negative weights use Bellman-Ford; for all-pairs use Floyd-Warshall.
- GATE tests Dijkstra trace, complexity comparison, and “why does it fail on negative edges”.
1. What Is Dijkstra’s Algorithm?
Dijkstra’s algorithm, published by Edsger W. Dijkstra in 1959, solves the Single-Source Shortest Path (SSSP) problem on a weighted graph with non-negative edge weights. Given a source vertex s, it computes the minimum-weight path from s to every other vertex.
Real-world applications: GPS navigation (Google Maps, OpenStreetMap), internet routing (OSPF protocol), flight route optimization, and network packet forwarding.
find d[v] = minimum total weight of any path from source s to vertex v, for all v ∈ V.
2. The Greedy Core Idea
Dijkstra’s algorithm maintains a set S of “settled” (finalized) vertices whose shortest distance from source is known. At each step it greedily picks the unsettled vertex with the smallest tentative distance, moves it into S, then relaxes all its outgoing edges.
if d[u] + w(u,v) < d[v]:
d[v] = d[u] + w(u,v)
Invariant: Once a vertex is settled, d[v] is the true shortest distance. (Proof relies on non-negative weights — no future edge can give a shorter path to an already-settled vertex.)
3. Pseudocode
DIJKSTRA(G, s):
for each vertex v in G:
d[v] = ∞
prev[v] = null
d[s] = 0
PQ = MinPriorityQueue() // (distance, vertex)
PQ.insert((0, s))
while PQ is not empty:
(dist_u, u) = PQ.extract_min()
if dist_u > d[u]: // stale entry — skip
continue
for each edge (u, v, w) in G.adj[u]:
if d[u] + w < d[v]:
d[v] = d[u] + w
prev[v] = u
PQ.insert((d[v], v))
return d, prevThe stale entry check on line 10 handles duplicate entries in the priority queue — a common lazy-deletion trick used in Python's heapq.
4. Step-by-Step Trace
Consider the weighted directed graph:
Vertices: A B C D E Edges (u → v, weight): A→B: 4 A→C: 1 B→D: 1 C→B: 2 C→D: 5 D→E: 3 B→E: 6
Source = A
| Step | Extract | d[A] | d[B] | d[C] | d[D] | d[E] | Action |
|---|---|---|---|---|---|---|---|
| Init | — | 0 | ∞ | ∞ | ∞ | ∞ | PQ: {(0,A)} |
| 1 | A (0) | 0 | 4 | 1 | ∞ | ∞ | Relax A→B=4, A→C=1. PQ:{(1,C),(4,B)} |
| 2 | C (1) | 0 | 3 | 1 | 6 | ∞ | Relax C→B: 1+2=3<4 ✓. C→D: 1+5=6. PQ:{(3,B),(4,B),(6,D)} |
| 3 | B (3) | 0 | 3 | 1 | 4 | 9 | Relax B→D: 3+1=4<6 ✓. B→E: 3+6=9. PQ:{(4,B-stale),(4,D),(9,E)} |
| 4 | B (4) — stale | — | — | — | — | — | d[B]=3 < 4, skip |
| 5 | D (4) | 0 | 3 | 1 | 4 | 7 | Relax D→E: 4+3=7<9 ✓. PQ:{(7,E),(9,E-stale)} |
| 6 | E (7) | 0 | 3 | 1 | 4 | 7 | No outgoing edges. Done. |
A=0, B=3, C=1, D=4, E=7
Shortest paths:
A→C (cost 1) | A→C→B (cost 3) | A→C→B→D (cost 4) | A→C→B→D→E (cost 7)
5. Python Implementation
import heapq
def dijkstra(graph, source):
"""
graph: dict of {node: [(neighbour, weight), ...]}
returns: dist dict, prev dict
"""
dist = {v: float('inf') for v in graph}
prev = {v: None for v in graph}
dist[source] = 0
pq = [(0, source)] # (distance, vertex)
while pq:
d, u = heapq.heappop(pq)
if d > dist[u]: # stale entry
continue
for v, w in graph[u]:
nd = dist[u] + w
if nd < dist[v]:
dist[v] = nd
prev[v] = u
heapq.heappush(pq, (nd, v))
return dist, prev
def shortest_path(prev, target):
path = []
while target is not None:
path.append(target)
target = prev[target]
return list(reversed(path))
# Example usage
graph = {
'A': [('B', 4), ('C', 1)],
'B': [('D', 1), ('E', 6)],
'C': [('B', 2), ('D', 5)],
'D': [('E', 3)],
'E': []
}
dist, prev = dijkstra(graph, 'A')
print(dist) # {'A':0,'B':3,'C':1,'D':4,'E':7}
print(shortest_path(prev, 'E')) # ['A','C','B','D','E']6. Time and Space Complexity
| Priority Queue Type | Extract Min | Decrease Key / Insert | Total Time | Best For |
|---|---|---|---|---|
| Unsorted Array | O(V) | O(1) | O(V²) | Dense graphs (E ≈ V²) |
| Binary Min-Heap | O(log V) | O(log V) | O((V+E) log V) | Sparse graphs (E ≈ V) |
| Fibonacci Heap | O(log V) | O(1) amortized | O(E + V log V) | Theoretical optimum |
Default Dijkstra complexity = O(V²) (array implementation, assumed unless stated).
With priority queue = O((V + E) log V) or O(E log V) for connected graphs.
Space: O(V) for the dist[] and prev[] arrays plus O(V + E) for the adjacency list.
7. Why Dijkstra Fails on Negative Weights
Consider this graph with source A:
A —(1)→ B —(−3)→ C A —(3)→ C
Dijkstra extracts A (dist=0), relaxes:
d[B] = 1, d[C] = 3. Settles B (dist=1).
Relaxes B→C: 1 + (−3) = −2.
BUT B is already settled — Dijkstra CANNOT go back to update C!
Dijkstra outputs d[C] = 3. Correct answer is −2. Wrong!
The greedy invariant — "once settled, a vertex has its final shortest distance" — breaks when negative edges exist, because a path found later via a negative edge can undercut a previously "finalized" result. Bellman-Ford avoids this by making V−1 full relaxation passes.
8. Dijkstra vs Bellman-Ford vs Floyd-Warshall
| Feature | Dijkstra | Bellman-Ford | Floyd-Warshall |
|---|---|---|---|
| Problem type | Single-source | Single-source | All-pairs |
| Negative weights | ❌ Fails | ✅ Handles | ✅ Handles |
| Negative cycles | ❌ | ✅ Detects | ✅ Detects |
| Time complexity | O((V+E) log V) | O(VE) | O(V³) |
| Space complexity | O(V) | O(V) | O(V²) |
| Approach | Greedy | Dynamic Programming | Dynamic Programming |
| Use when | Non-negative weights, sparse graphs | Negative weights exist | Need all-pair distances |
9. GATE-Level Worked Examples
Example 1 — Dijkstra Trace (GATE 2021 style)
Edges: (A,B,2), (A,C,6), (B,C,3), (B,D,8), (C,D,2), (D,E,4), (C,E,9)
Q: Find shortest distances from A to all vertices using Dijkstra.
Solution (settle order):
Settle A(0): d[B]=2, d[C]=6 Settle B(2): d[C]=min(6,2+3)=5, d[D]=min(∞,2+8)=10 Settle C(5): d[D]=min(10,5+2)=7, d[E]=min(∞,5+9)=14 Settle D(7): d[E]=min(14,7+4)=11 Settle E(11): done
Result: A=0, B=2, C=5, D=7, E=11
Example 2 — Complexity Choice (GATE MCQ pattern)
Array version: O(V²) = 10⁶ operations
Heap version: O(E log V) = 400,000 × 10 = 4,000,000 operations
Answer: Array (O(V²)) is faster for this dense graph.
The heap version wins only when E = O(V log V) or less (sparse graphs).
Example 3 — Path Reconstruction
prev[E]=D, prev[D]=C, prev[C]=B, prev[B]=A, prev[A]=null
Reading backwards: E ← D ← C ← B ← A
Shortest path: A → B → C → D → E (cost = 11)
10. Five Common Mistakes
Mistake 1 — Using Dijkstra on negative-weight graphs
Dijkstra's greedy invariant breaks the moment any edge weight is negative. If the problem states "edge weights can be negative," switch immediately to Bellman-Ford.
Mistake 2 — Not handling stale priority queue entries
Python's heapq has no decrease-key operation. When you update d[v], you push a new entry. If you don't check if d_pop > dist[u]: continue, you'll reprocess settled vertices and corrupt your results.
Mistake 3 — Quoting wrong complexity in GATE
Many students write O(E log V) and forget the +V term. The correct formula is O((V + E) log V). For connected graphs E ≥ V−1 so E dominates, but GATE may test the exact expression.
Mistake 4 — Confusing single-source vs all-pairs
Dijkstra gives shortest paths FROM ONE source to all others. If a question asks for shortest paths between ALL pairs of vertices, use Floyd-Warshall O(V³) — not V repeated Dijkstra runs (which gives O(V(V+E) log V), often slower for dense graphs).
Mistake 5 — Assuming Dijkstra works on directed vs undirected differently
The algorithm is identical for directed and undirected graphs. For undirected edges, simply add both directions to the adjacency list. The logic for relaxation and settling does not change.
11. Frequently Asked Questions
Why does Dijkstra's algorithm fail on negative weight edges?
Dijkstra is greedy — once a vertex is settled (extracted from the priority queue), its distance is never updated again. With negative edges, a path found later through a negative-weight edge could produce a shorter total distance to an already-settled vertex. Bellman-Ford avoids this by performing V−1 full passes, allowing every vertex's distance to be updated repeatedly.
What is the time complexity of Dijkstra's algorithm?
With a binary min-heap: O((V + E) log V). With a Fibonacci heap: O(E + V log V). With a simple unsorted array: O(V²). For dense graphs (E ≈ V²), the array version can be faster. For sparse graphs, the heap version wins. GATE typically expects O(V²) as the default unless "priority queue" is explicitly mentioned.
What is the difference between Dijkstra and Bellman-Ford?
Dijkstra: greedy, O((V+E) log V), no negative weights. Bellman-Ford: dynamic programming, O(VE), handles negative weights, detects negative cycles. Choose Dijkstra for GPS/routing (non-negative real-world weights). Choose Bellman-Ford when negative weights exist (financial graphs, altitude maps).
Is Dijkstra's algorithm the same as BFS?
No. BFS finds the shortest path by number of edges using a simple Queue — O(V+E). Dijkstra finds the shortest path by total edge weight using a Priority Queue — O((V+E) log V). If all edge weights equal 1, both give the same result, but BFS is simpler and faster. Dijkstra is effectively a weighted generalization of BFS.