Programming / Algorithms
Tutorial

How to Implement Dijkstra's Algorithm for Shortest Paths

Implement Dijkstra’s algorithm for non-negative shortest-path problems with a practical workflow: prerequisites, data structures, step-by-step execution, validation, and production checks.

How to Implement Dijkstra's Algorithm for Shortest Paths

Introduction

When you need the shortest path from one source to many destinations, the operational question is not just which route is mathematically shortest, but whether the implementation is correct, efficient, and safe for the graph you actually have. Dijkstra’s algorithm solves this problem for graphs with non-negative edge weights, which makes it a common fit for routing, dependency graphs, costed infrastructure paths, and other systems where every hop has a measurable cost.

In this tutorial, you will build a practical implementation of Dijkstra’s algorithm, understand what it returns, validate the result against sample inputs, and check the conditions that must be true before you use it in a production workflow. If your use case is network routing, the same shortest-path logic appears in Dijkstra's Algorithm for Fastest Path Routing in Networks, but this article stays focused on implementation.

What you will build

You will implement a single-source shortest-path routine that:

  • accepts a weighted graph and a start node,
  • computes the minimum total cost to every reachable node,
  • records predecessor links so you can reconstruct the actual path,
  • fails fast if the graph contains negative weights,
  • and produces outputs that can be validated before deployment.

The finished state should be a function or module that returns both distance values and path reconstruction data, with enough checks to tell whether the result is trustworthy.

Prerequisites and stop-here checks

Before you write code, confirm the problem really fits Dijkstra’s algorithm.

Goal

Avoid implementing the wrong shortest-path method for a graph that violates the algorithm’s assumptions.

Action

Verify these conditions:

  1. All edge weights are non-negative.
  2. You need the shortest path from one source node, not all-pairs shortest paths.
  3. The graph structure is known and can be represented in memory, or you have a bounded way to stream it.
  4. “Shortest” means minimum accumulated cost, not minimum number of hops.

Expected output

A clear go/no-go decision:

  • Go: non-negative weighted graph, single-source shortest paths.
  • Stop here: negative edges, dynamic weights that can change during execution, or a need for a different optimization objective.

Validation

Inspect sample edge definitions and confirm no negative values appear. If you are reading the graph from configuration or a data store, add a schema or runtime check that rejects negative weights before the algorithm starts.

Common failure

A frequent mistake is to treat Dijkstra’s algorithm as a general shortest-path solver. It is not safe with negative-weight edges. If negative weights are possible, stop and use a different algorithm instead of trying to patch the implementation.

Choose a graph representation

Goal

Use a data structure that supports efficient neighbor lookup and predictable runtime.

Action

Represent the graph as an adjacency list rather than an adjacency matrix for most practical sparse graphs. In an adjacency list, each node maps to a list of outgoing edges and weights.

Example graph:

graph = {
    "A": [("B", 4), ("C", 2)],
    "B": [("C", 1), ("D", 5)],
    "C": [("D", 8), ("E", 10)],
    "D": [("E", 2), ("F", 6)],
    "E": [("F", 3)],
    "F": []
}

Expected output

A graph structure where each node can enumerate its neighbors and associated costs efficiently.

Validation

Check that every edge is stored with exactly one numeric weight and that every referenced node exists or is intentionally treated as implicit. If you are integrating with another system, make sure node identifiers are stable and consistent across inputs.

Common failure

Using an adjacency matrix for a sparse graph can waste memory and slow iteration over neighbors. Another common failure is storing edges without a consistent node identifier format, which makes path reconstruction harder later.

Understand the algorithm before coding

Goal

Know the operational flow so the implementation matches the theory.

Action

Dijkstra’s algorithm maintains a tentative distance for each node and repeatedly selects the not-yet-finalized node with the smallest distance. From that node, it relaxes outgoing edges: if going through the current node creates a shorter route to a neighbor, update the neighbor’s distance and predecessor.

The key property is that once a node is finalized, its shortest distance is known and will not improve, assuming all edges are non-negative.

Expected output

A mental model for the implementation:

  • initialize distances,
  • select the smallest tentative node,
  • relax adjacent edges,
  • mark the node as processed,
  • repeat until no useful nodes remain.

Validation

You should be able to explain why a finalized node does not need to be revisited in a non-negative graph. If that assumption is not true for your data, the algorithm is not the right fit.

Common failure

A common error is to relax edges but forget to keep predecessor information. Without predecessors, you get distances but cannot reconstruct the actual path.

Implement the shortest-path routine

Goal

Create a working implementation that returns both distances and paths.

Action

Use a priority queue to always process the node with the current smallest tentative distance. In Python, heapq is a practical standard-library option. The following implementation is compact, readable, and suitable as a reference pattern.

import heapq
from math import inf


def dijkstra(graph, start):
    distances = {node: inf for node in graph}
    previous = {node: None for node in graph}
    distances[start] = 0

    priority_queue = [(0, start)]
    visited = set()

    while priority_queue:
        current_distance, current_node = heapq.heappop(priority_queue)

        if current_node in visited:
            continue
        visited.add(current_node)

        for neighbor, weight in graph[current_node]:
            if weight < 0:
                raise ValueError("Dijkstra's algorithm requires non-negative weights")

            new_distance = current_distance + weight
            if new_distance < distances[neighbor]:
                distances[neighbor] = new_distance
                previous[neighbor] = current_node
                heapq.heappush(priority_queue, (new_distance, neighbor))

    return distances, previous


def build_path(previous, start, target):
    path = []
    current = target

    while current is not None:
        path.append(current)
        if current == start:
            break
        current = previous[current]

    if not path or path[-1] != start:
        return None

    return list(reversed(path))

Expected output

A function that returns:

  • distances: the minimum known cost from the source to each node,
  • previous: a parent map for path reconstruction,
  • build_path(...): a helper that converts the parent map into a concrete route.

Validation

Run the function against a small graph and inspect both the numeric output and the recovered path. For the sample graph above, starting from A, you should expect A -> B -> D -> E -> F to be cheaper than routes that go through C directly to E.

Common failure

Two implementation errors show up often:

  • forgetting to ignore stale queue entries, which can create redundant work,
  • assuming every node appears as a key in the graph dictionary, which breaks if leaf nodes are implicit.

If your graph can contain nodes that only appear as edge targets, normalize the input first so every node has an entry.

Walk through a concrete example

Goal

Verify algorithm behavior on a small graph before scaling up.

Action

Consider this graph from source A:

  • A -> B cost 4
  • A -> C cost 2
  • B -> C cost 1
  • B -> D cost 5
  • C -> D cost 8
  • C -> E cost 10
  • D -> E cost 2
  • D -> F cost 6
  • E -> F cost 3

The algorithm proceeds by expanding the lowest tentative distance at each step:

  1. Start at A with distance 0.
  2. Discover B=4 and C=2.
  3. Process C next, updating D=10 and E=12.
  4. Process B, which improves D to 9.
  5. Process D, which improves E to 11 and F to 15.
  6. Process E, which improves F to 14.

Expected output

Final shortest distances from A:

  • A: 0
  • C: 2
  • B: 4
  • D: 9
  • E: 11
  • F: 14

And the reconstructed shortest path to F should be A -> B -> D -> E -> F.

Validation

Compare the returned distance map to expected hand-calculated values on the same graph. Also verify that the path reconstructed from previous matches the reported distance when you sum its edges.

Common failure

If your output shows a shorter route to F but the path sum does not match the distance, the predecessor map is inconsistent or the graph data was modified mid-run.

Validate the implementation

Goal

Confirm the implementation behaves correctly on representative test cases.

Action

Use a small validation set that covers the main scenarios:

  • a simple connected graph,
  • a graph with an unreachable node,
  • a graph with multiple paths to the same destination,
  • a graph containing a zero-weight edge,
  • and a graph with a negative edge that must be rejected.

You can validate with a minimal test script:

graph = {
    "A": [("B", 4), ("C", 2)],
    "B": [("C", 1), ("D", 5)],
    "C": [("D", 8), ("E", 10)],
    "D": [("E", 2), ("F", 6)],
    "E": [("F", 3)],
    "F": []
}

distances, previous = dijkstra(graph, "A")
print(distances)
print(build_path(previous, "A", "F"))

Expected output

A consistent distance dictionary, plus a valid path reconstruction for reachable nodes.

Validation

Use these checks:

  • The distance to the start node is zero.
  • Every improved node has a valid predecessor, except the start node.
  • Reconstructed paths have a total cost equal to the stored distance.
  • Unreachable nodes remain at infinity or another explicit sentinel.
  • Negative edges raise an error before producing a result.

If you are building this into a larger system, it is useful to compare the algorithm output against a known-good reference on a small fixture set. For network-oriented problem framing, the same validation discipline applies as in routing use cases described in the routing-focused article linked above.

Common failure

A common validation gap is only checking one “happy path” result. That does not prove the implementation handles unreachable nodes, duplicate relaxations, or invalid input.

Operational follow-up after implementation

Goal

Prepare the algorithm for use in a controlled production workflow.

Action

Before deployment or integration, verify the following operational points:

  • Input hygiene: reject negative weights and malformed edges early.
  • Graph stability: ensure the graph does not mutate while the algorithm is running.
  • Node coverage: normalize nodes that appear only as targets.
  • Performance expectations: confirm the graph size is appropriate for an in-memory single-source shortest-path run.
  • Result handling: decide whether you need only distances or also reconstructed paths.

If your implementation will be called from automation or a service endpoint, add logging for rejected inputs and a clear error path for missing start nodes. If you are using the result as part of a larger network or system workflow, this is also a good moment to compare whether the problem resembles route selection or a different optimization problem entirely; for example, path selection in a network can overlap conceptually with socket-based connectivity workflows such as those discussed in Python Socket Programming Tutorial for Network Communication, but the graph model and validation rules are still distinct.

Expected output

A shortest-path component that can be safely embedded into another tool, job, or service with predictable input checks and output semantics.

Validation

Perform a production-readiness review:

  • Can invalid weights be rejected before execution?
  • Are distances and predecessor data persisted or returned in the format your caller expects?
  • Is there monitoring or logging for impossible states, such as a missing start node?
  • Have you confirmed that the graph size and update pattern fit the chosen data structure?

Common failure

The most common operational mistake is assuming the algorithm is “done” once it returns numbers. In practice, you also need guardrails around input quality, graph mutation, and result interpretation.

When not to use Dijkstra's algorithm

Goal

Avoid forcing the method into a problem it does not solve well.

Action

Do not use this algorithm if:

  • edge weights may be negative,
  • the graph changes during computation in ways that invalidate tentative distances,
  • you need all-pairs shortest paths rather than one source,
  • or the true objective is not additive cost.

Expected output

A correct decision about fit, rather than an overconfident implementation.

Validation

Ask whether the result depends on a property that Dijkstra’s algorithm guarantees only for non-negative edges. If yes, the method is suitable. If no, choose a different algorithm.

Common failure

The most dangerous failure is silent misuse on the wrong graph type. The code can still run and produce a number, but the number may not be correct.

Final takeaway

Dijkstra’s algorithm is straightforward to implement, but it is only reliable when the graph satisfies its assumptions and the implementation preserves them. If you use a priority queue, track predecessors, reject negative weights, and validate the output on a small fixture set, you get a practical shortest-path routine that is easy to reason about and safe to integrate.

The best production rule is simple: implement it for non-negative single-source shortest-path problems, verify it with known paths and unreachable-node cases, and stop immediately if your data violates the algorithm’s requirements.

Use this guidance together with Node.js API rate limiting and JWT authentication in ASP.NET Core to connect the workflow with related operational context already available on the site.

Part of the Programming: Algorithms Insights content cluster.

Continue learning

Related content