Understanding Dijkstra’s Algorithm for Shortest Path
Dijkstra’s algorithm is a cornerstone of graph theory and computer science, designed to find the shortest path from a source node to all other nodes in a weighted graph. Developed by Edsger W. Dijkstra in 1956, it remains one of the most frequently discussed algorithms in technical interviews. For developers preparing for coding assessments, a solid grasp of Dijkstra’s algorithm is essential, as it appears in a wide range of problems from network routing to game development. This article provides a detailed walkthrough of the algorithm, complete with conceptual diagrams and a clean Python implementation that can be directly adapted for interview practice.
The algorithm addresses the single-source shortest path problem under the condition that all edge weights are non-negative. It operates on the principle of greedy expansion: it repeatedly selects the unvisited node with the smallest known distance, explores its neighbors, and updates their distances if a shorter path is found. This process continues until all nodes have been visited or the distances to all reachable nodes have been finalized. Understanding this process is crucial for both writing correct implementations and optimizing them using appropriate data structures.
By the end of this article, readers will be able to implement Dijkstra’s algorithm from scratch, explain its logic step by step, and recognize common pitfalls such as handling graphs with negative weights or disconnected components. The content is structured to bridge theoretical concepts with practical coding skills, making it particularly useful for those preparing for technical interviews. Let us begin by defining the algorithm and its core mechanics.
What is Dijkstra’s Algorithm?
Dijkstra’s algorithm is a deterministic algorithm that computes the shortest paths from a single source node to every other node in a graph. The input consists of a graph represented as a set of nodes and edges, where each edge has a non-negative weight. The output is a set of distances indicating the shortest distance from the source to each node, along with the predecessor of each node that can be used to reconstruct the actual path. The algorithm is widely used in applications such as GPS navigation, network routing protocols like OSPF, and many other optimization scenarios.
One of the key features of Dijkstra’s algorithm is its use of a priority queue to efficiently select the next node to process. Initially, all nodes are assigned a tentative distance value—zero for the source and infinity for all others. The algorithm then repeatedly extracts the node with the smallest tentative distance from the priority queue, marks it as visited, and examines its neighbors. For each neighbor, it calculates a potential new distance by adding the current node’s distance to the weight of the connecting edge. If this new distance is smaller than the neighbor’s current tentative distance, the neighbor’s distance is updated and the priority queue is adjusted accordingly.
This process is known as edge relaxation and is the heart of the algorithm. Because all edge weights are non-negative, once a node’s distance is finalized (i.e., removed from the priority queue), it will never be decreased later. This property guarantees the correctness of the algorithm and ensures that the greedy selection works optimally.
Step-by-Step Execution with a Concrete Example
To visualize the algorithm, consider a small undirected graph with nodes A, B, C, D, and E. Edges: A-B weight 4, A-C weight 2, B-C weight 1, B-D weight 5, C-D weight 8, C-E weight 10, D-E weight 2. Suppose the source node is A. The initial distances are: A=0, others=infinity. The priority queue contains (0, A). The steps are as follows:
- Step 1: Extract (0, A). Mark A as visited. Neighbors: B and C. Relax edges: B distance = min(inf, 0+4)=4, C distance = 0+2=2. Update priority queue: (2, C), (4, B).
- Step 2: Extract (2, C). Mark C visited. Neighbors: A (already visited), B, D, E. Relax: B = min(4, 2+1)=3, D = min(inf, 2+8)=10, E = 2+10=12. Update queue: (3, B), (4, B), (10, D), (12, E).
- Step 3: Extract (3, B). Mark B visited. Neighbors: A (visited), C (visited), D. Relax D: D = min(10, 3+5)=8. Update queue: (8, D), (10, D) still present. Queue now contains (4, B) but B visited.
- Step 4: Extract (4, B) – but B visited, skip. Next smallest is (8, D). Extract D. Mark visited. Neighbors: C (visited), B (visited), E. Relax E: E = min(12, 8+2)=10. Update queue: (10, E), (12, E).
- Step 5: Extract (10, E). Mark visited. Done. Final distances: A=0, B=3, C=2, D=8, E=10.
This example illustrates how the algorithm progresses, and the final distances can be verified manually. The path reconstruction would show the shortest route from A to each node.
Key Data Structures: Priority Queue and Relaxation
The efficiency of Dijkstra’s algorithm depends heavily on the data structure used for the priority queue. A binary heap, available in Python’s heapq module, provides O(log n) insertion and extraction, resulting in an overall complexity of O((V+E) log V) where V is the number of vertices and E is the number of edges. For dense graphs, using a Fibonacci heap can reduce the theoretical complexity to O(E + V log V), but the constant factors often make binary heap preferable in practice. During interviews, using a binary heap is typically sufficient.
Edge relaxation is the operation of updating the shortest distance to a node by considering a new path through an adjacent node. The condition is straightforward: if the distance to the current node plus the edge weight is less than the neighbor’s current distance, then the neighbor’s distance is updated. This operation is performed for each outgoing edge of the node being processed. In the Python implementation, this is implemented with a simple if statement inside a loop.
Another important concept is the use of a visited set or distance comparison to handle duplicate entries in the priority queue. Since multiple distance updates for the same node may result in multiple entries in the heap, it is common to check whether the popped distance matches the node’s current distance in the distances dictionary. If it is larger, the entry is stale and can be ignored. This avoids the need to explicitly decrease-key operations, which are not supported by Python’s heap.
Python Implementation of Dijkstra’s Algorithm
Below is a compact implementation of Dijkstra’s algorithm using Python’s heapq. The graph is represented as a dictionary where keys are node names and values are dictionaries of neighbor-weight pairs. The function returns a dictionary of shortest distances from the start node.
import heapq
def dijkstra(graph, start):
distances = {node: float(‘inf’) for node in graph}
distances[start] = 0
priority_queue = [(0, start)]
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
if current_distance > distances[current_node]:
continue
for neighbor, weight in graph[current_node].items():
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(priority_queue, (distance, neighbor))
return distances
The implementation begins by initializing all distances to infinity except the start node. It then pushes the start node into the priority queue. The while loop runs until the queue is empty. In each iteration, the node with the smallest tentative distance is popped. If the popped distance is greater than the recorded distance (due to stale entries), the node is skipped. Otherwise, for each neighbor, a new distance is computed and compared. If improved, the neighbor’s distance is updated and a new entry is pushed onto the heap. Note that this code does not explicitly track visited nodes; stale entries naturally handle that.
To reconstruct the actual shortest path, a predecessor dictionary can be maintained similarly, updating it each time a distance is improved. After the algorithm completes, the path can be traced from the target node back to the source by following predecessors.
Time and Space Complexity
The time complexity of Dijkstra’s algorithm with a binary heap is O((V + E) log V). The extraction of the minimum node occurs V times, each taking O(log V). Each edge may be relaxed once, and each relaxation may cause a push onto the heap, also O(log V). Thus the total is O(V log V + E log V) which simplifies to O((V+E) log V). In the worst-case dense graph where E ≈ V^2, this becomes O(V^2 log V). However, for sparse graphs, the performance is near-linear.
Space complexity is O(V) for storing distances and predecessors, plus O(V) for the priority queue in the worst case (each node may be pushed multiple times). In practice, the queue size is bounded by the number of edges or V. For graphs with non-negative weights, this implementation is both efficient and straightforward to write during an interview.
Common Pitfalls and Interview Considerations
One common mistake is attempting to use Dijkstra’s algorithm on graphs with negative edge weights. The algorithm assumes non-negative weights; negative edges can cause incorrect results because once a node’s distance is finalized, it might later be reduced by a negative edge from an unvisited node. For graphs with negative weights, the Bellman-Ford algorithm is appropriate. Another pitfall is forgetting to handle disconnected or unreachable nodes. In the implementation, unreachable nodes will retain a distance of infinity, which should be communicated clearly in the output.
During coding interviews, interviewers often ask about the choice of data structure. Discussing the trade-offs between a binary heap and a Fibonacci heap demonstrates depth of understanding. Additionally, questions may involve reconstructing the path, handling directed versus undirected graphs, or adapting the algorithm for multiple source nodes. Practicing writing the algorithm from memory and debugging edge cases can be beneficial.
Readers can also explore additional algorithm problems on platforms like CodeStack Studio to reinforce their understanding through interactive exercises and community discussions. While this article provides a foundation, consistent practice remains key to mastering graph algorithms.