Dijkstra’s Algorithm Explained: Step-by-Step with Examples

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”.

Advertisement

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.

Problem Statement: Given G = (V, E, w) where w(u,v) ≥ 0 for all edges,
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.

Relaxation: For edge (u, v) with weight w:
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, prev

The 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

StepExtractd[A]d[B]d[C]d[D]d[E]Action
Init0PQ: {(0,A)}
1A (0)041Relax A→B=4, A→C=1. PQ:{(1,C),(4,B)}
2C (1)0316Relax C→B: 1+2=3<4 ✓. C→D: 1+5=6. PQ:{(3,B),(4,B),(6,D)}
3B (3)03149Relax B→D: 3+1=4<6 ✓. B→E: 3+6=9. PQ:{(4,B-stale),(4,D),(9,E)}
4B (4) — staled[B]=3 < 4, skip
5D (4)03147Relax D→E: 4+3=7<9 ✓. PQ:{(7,E),(9,E-stale)}
6E (7)03147No outgoing edges. Done.
Final shortest distances from A:
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']

Advertisement

6. Time and Space Complexity

Priority Queue TypeExtract MinDecrease Key / InsertTotal TimeBest For
Unsorted ArrayO(V)O(1)O(V²)Dense graphs (E ≈ V²)
Binary Min-HeapO(log V)O(log V)O((V+E) log V)Sparse graphs (E ≈ V)
Fibonacci HeapO(log V)O(1) amortizedO(E + V log V)Theoretical optimum
GATE Rule:
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

FeatureDijkstraBellman-FordFloyd-Warshall
Problem typeSingle-sourceSingle-sourceAll-pairs
Negative weights❌ Fails✅ Handles✅ Handles
Negative cycles✅ Detects✅ Detects
Time complexityO((V+E) log V)O(VE)O(V³)
Space complexityO(V)O(V)O(V²)
ApproachGreedyDynamic ProgrammingDynamic Programming
Use whenNon-negative weights, sparse graphsNegative weights existNeed all-pair distances
Advertisement

9. GATE-Level Worked Examples

Example 1 — Dijkstra Trace (GATE 2021 style)

Graph (undirected, weighted):

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)

Q: For a dense graph with V=1000 vertices and E=400,000 edges, which Dijkstra implementation is faster?

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

Q: Using the prev[] array from Example 1, reconstruct the shortest path from A to E.

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.

12. Next Steps

Advertisement

Leave a Comment