Key Takeaways: Finding the Shortest Path between two nodes is one of the most practically useful applications of Graph Data Structures. The industry standard for finding this in graphs with non-negative weights is Dijkstra's Algorithm.
In our previous tutorials, we covered how to traverse graphs layer by layer using Breadth-First Search (BFS). While BFS is fantastic for finding the shortest path in an unweighted graph, real-world data is rarely that simple.
What happens when the edges have different costs, distances, or times attached to them? You need an algorithm that can mathematically weigh the options.
Shortest path algorithms power the systems that drive the modern world. Here are just a few real-life applications:
Created by computer scientist Edsger W. Dijkstra in 1956, this algorithm guarantees the absolute shortest path from a starting node to all other nodes in a Weighted Directed or Undirected Graph—as long as there are no negative edge weights.
Instead of blindly exploring every node like BFS, Dijkstra's algorithm uses a Greedy Approach. It constantly asks: "Of all the nodes I can currently reach, which one is the cheapest to get to?"
A Directed Weighted Graph. The Shortest Path from A to E is highlighted, successfully avoiding the misleadingly simple direct routes.
To efficiently find the cheapest next step, Dijkstra's algorithm relies on a Min-Heap (Priority Queue) and a table of distances.
0, and all other nodes to Infinity.0.By the end of this loop, your distance table will contain the absolute minimum cost required to reach every single node in the graph from your starting point!
Python's heapq module is perfect for the Priority Queue required by Dijkstra's algorithm. Let's write the code to solve the exact graph illustrated in the image above.
import heapqdef dijkstra(graph, start): # 1. Initialize distances to Infinity distances = {node: float('inf') for node in graph} distances[start] = 0 # Priority Queue holds tuples of (current_cost, node) priority_queue = [(0, start)] while priority_queue: # 2. Pop the node with the lowest cost current_distance, current_node = heapq.heappop(priority_queue) # Optimization: Ignore if we already found a better path earlier if current_distance > distances[current_node]: continue # 3. Check all adjacent neighbors for neighbor, weight in graph[current_node].items(): distance = current_distance + weight # 4. If new path is cheaper, update and push to queue if distance < distances[neighbor]: distances[neighbor] = distance heapq.heappush(priority_queue, (distance, neighbor)) return distances
# Graph from the SVG diagram (Adjacency List with Dictionary) graph = { 'A': {'B': 4, 'C': 2}, 'B': {'D': 5}, 'C': {'B': 1, 'D': 8, 'E': 10}, 'D': {'E': 2}, 'E': {} }
shortest_paths = dijkstra(graph, 'A') print("Shortest path costs from A:", shortest_paths) # Output: {'A': 0, 'B': 3, 'C': 2, 'D': 8, 'E': 10}
The basic implementation above gives you the minimum cost to reach every node. But what if your application actually needs to print the physical route (e.g., A -> C -> B -> D -> E)?
To achieve this, you simply need to keep track of the "Previous Node" every time you update a cheaper distance. Once the algorithm finishes, you can trace backward from your destination to your start.
Inside the update block (Step 4), simply add:
previous_nodes[neighbor] = current_node
The time complexity of Dijkstra heavily depends on the data structure used to find the minimum distance node. By using a Binary Min-Heap (like Python's heapq), we achieve stellar performance:
O((V + E) log V) — We extract the minimum vertex up to V times, and we push onto the heap up to E times. Each heap operation takes log V time.O(V) — For the distances dictionary and the priority queue size.Dijkstra's Algorithm operates on the fundamental assumption that adding an edge to a path never makes the path shorter. It assumes all weights are ≥ 0.
If your graph contains Negative Edge Weights (e.g., a financial graph where an edge represents a profit or tax rebate), Dijkstra's Greedy Approach will fail and return incorrect results.
If you suspect your graph has negative weights, you must abandon Dijkstra and instead use the Bellman-Ford Algorithm, which is slightly slower but safely handles negative numbers.
Which data structure is required to make Dijkstra's Algorithm run efficiently?
Under what condition will Dijkstra's Algorithm fail to guarantee the correct shortest path?