Dijkstra's algorithm
Find the cheapest path from one start to every node when edges have non-negative costs, always growing from the closest unfinished node.
O((n + e) log n)Space O(n + e)5 practice problemsStep-through pictureThe idea
When edges have different costs, such as travel times or prices, the path with the fewest edges is not always the cheapest. Dijkstra's algorithm finds the cheapest total cost from a start node to every other node, as long as no edge has a negative cost.
It keeps a best-known distance dist for every node. At each step it picks the unfinished node with the smallest dist. That distance can't get any better, because every other route would have to go through a node that is already at least as far away and then add a cost that is 0 or more. So that node is finished, and it tries to improve its neighbors' distances.
Picking the smallest quickly needs a priority queue, a structure that always hands back its smallest item. JavaScript doesn't have one built in, so the template includes a small binary heap with push and pop.
An everyday example
Water flowing through pipes
Pour water into one end of a network of pipes, where longer pipes take longer to fill. The water reaches each junction at the earliest possible time, and at that moment it starts flowing into every pipe leaving that junction. Junctions fill up in order of how quickly water can get to them. Dijkstra's algorithm visits nodes in that same order.
| In the example | In the code |
|---|---|
| The pipe junctions | Nodes |
| How long a pipe takes to fill | An edge's weight |
| The earliest time water reaches a junction | dist[node] |
| The junction that fills next | pop(heap), the smallest d |
| Water entering the next pipes | Trying d + weight for each neighbor |
From the slow way to dijkstra's algorithm
- Start with the obvious way: list every path from the start to each node, add up each path's cost, and keep the smallest. The number of paths grows exponentially with the size of the graph. BFS is fast, but it finds the path with the fewest edges, and with costs a three-edge path costing 3 beats a one-edge path costing 10.
- Cheapest paths are made of cheapest paths. If the cheapest route to
Cgoes throughB, then the part that ends atBis also the cheapest route toB. So once we knowB's final cost, we can offer each neighbor the pricedist[B] + weightand never need to look at other routes intoBagain. - Which node's cost is final first? The start, at 0. After that, look at every node reached so far and take the one with the smallest known cost. Any other route to it must leave the finished nodes through a node that costs at least as much, then add edges that cost 0 or more, so it can't be cheaper. Finish that node, offer prices to its neighbors, and repeat.
- Scanning all nodes for the smallest cost takes
nsteps each time. A min-heap hands back the smallest entry inO(log n)instead. Rather than finding and changing an entry inside the heap when a price drops, push a new entry and skip the old one when it comes out later. A heap of[d, node]pairs, popped until empty, with outdated entries skipped: that loop is Dijkstra's algorithm.
How it works
- Build
graphwith[to, weight]pairs. Set everydisttoInfinity, exceptdist[start] = 0. - Put
[0, start]in the min-heapheap. - Pop the entry
[d, node]with the smallestd. - If
dis bigger thandist[node], it is an old entry from before a cheaper route was found: skip it. - For each edge
[next, weight], ifd + weightis smaller thandist[next], store it and push[dist[next], next]. - When the heap is empty,
distholds the cheapest cost fromstartto every node, orInfinityif unreachable.
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function dijkstra(n, edges, start) {const graph = Array.from({ length: n }, () => []);for (const [from, to, weight] of edges) graph[from].push([to, weight]);const dist = new Array(n).fill(Infinity);dist[start] = 0;const heap = [[0, start]];while (heap.length > 0) {const [d, node] = pop(heap);if (d > dist[node]) continue;for (const [next, weight] of graph[node]) {if (d + weight < dist[next]) {dist[next] = d + weight;push(heap, [dist[next], next]);}}}return dist;}function push(heap, item) {heap.push(item);let i = heap.length - 1;while (i > 0 && heap[(i - 1) >> 1][0] > heap[i][0]) {const parent = (i - 1) >> 1;[heap[parent], heap[i]] = [heap[i], heap[parent]];i = parent;}}function pop(heap) {const top = heap[0];const last = heap.pop();if (heap.length === 0) return top;heap[0] = last;let i = 0;while (true) {let small = i;for (const child of [2 * i + 1, 2 * i + 2]) {if (child < heap.length && heap[child][0] < heap[small][0]) small = child;}if (small === i) return top;[heap[small], heap[i]] = [heap[i], heap[small]];i = small;}}
How fast it is
| Time | O((n + e) log n) |
|---|---|
| Extra memory | O(n + e) |
Each edge can push one heap entry, and each push or pop costs O(log n) because the heap is a balanced tree. The graph, dist, and the heap hold O(n + e) entries.
- Best case
O(n + e)A simple chain like
0 → 1 → 2 → 3: the heap never holds more than one entry, so each push and pop is constant, and buildinggraphdominates. - Average case
O((n + e) log n)A typical graph, where the heap holds many entries and each push or pop costs about
log nswaps. - Worst case
O((n + e) log n)A dense graph where most edges lower a price, so nearly every edge pushes an entry. The heap can hold up to
eentries, andlog eis at most2 log n.
Common mistakes
- Using it with negative edge costs. A node can be finished too early; use Bellman-Ford instead.
- Forgetting the
if (d > dist[node]) continuecheck. Old entries then relax edges again, which wastes time. - Sorting an array on every step instead of using a heap. That turns each step into
O(n log n). - Marking a node as finished when it is pushed instead of when it is popped. A cheaper route can still be found after the push.
- Returning
Infinityin a problem that wants-1for unreachable nodes. Convert at the end.
When to use it
Look for these clues in a problem:
- Edges have costs, times, or distances, and the question asks for the cheapest, fastest, or shortest total.
- All costs are 0 or positive.
- You need the cheapest path from one start to one or all nodes.
- The "cost" of a path is built step by step, such as the largest step so far or a product of probabilities, and never gets better as the path grows.
Check yourself
Answer each one in your head first, then open it.
Why must a node be marked finished when it's popped, not when it's pushed?
A cheaper route can still be found after the push, like node 1 in the trace: pushed at 6, finished at 5. Only the pop of the smallest entry proves the cost is final.
How many entries can the heap hold at once?
Up to one per edge that lowered a price, plus the start, so O(e). That's why the time has log e, which is at most 2 log n.
What goes wrong if the if (d > dist[node]) continue line is removed?
Old entries would relax their node's edges again with a worse d. The final answer stays correct because those offers lose, but the work can grow a lot.
Why is the < in d + weight < dist[next] strict?
With <=, a tie would push a duplicate entry that improves nothing, as with node 4 at 14 in the trace. Strict < keeps the heap smaller.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 5 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
Two rules hold the whole time. First, every dist value is the cost of some real path, so it is never lower than the true cheapest cost. Second, when an entry [d, node] comes out of the heap and isn't skipped, d is the true cheapest cost of node.
Here's why the second rule holds. Suppose some path P to node costs less than d. P starts at a finished node and ends at node, so it has a first node Y that isn't finished. The node just before Y was finished, and at that moment it offered Y a price no bigger than the cost of P up to Y. Since every edge costs 0 or more, that part of P costs no more than all of P, which is less than d. So the heap holds an entry for Y smaller than d, and it would have come out before [d, node]. That contradicts how a min-heap works.
The loop ends because each push comes from a strict drop of some dist[next], and each node relaxes its edges only once, when it's finished. That's at most one push per edge, plus the start. When the heap is empty, every node that can be reached has been finished at its true cost, and every other node still has Infinity.
Going deeper
3 more ideas
Why the smallest popped distance is final
When [d, node] is the smallest entry in the heap, every other route to node must leave the finished area through some node whose distance is at least d, and then add edges that cost 0 or more. So no other route can cost less than d. This is why negative edges break the algorithm: a negative edge later on could make a "finished" node cheaper.
How the heap keeps it fast
A binary heap stores items in an array so that each item is no bigger than its two children, at indexes 2i + 1 and 2i + 2. The smallest item is always at index 0. push adds at the end and swaps it up while it is smaller than its parent; pop moves the last item to the top and swaps it down. Both take O(log n), so the whole algorithm is O((n + e) log n).
Stopping early and rebuilding the path
If you only need the cost to one target, return as soon as the target is popped: its distance is final. To rebuild the route itself, keep a prev array and set prev[next] = node whenever dist[next] improves. Then walk prev back from the target.
Work it by hand
Every step in a table, with why
Six nodes from start 0. Node 1 gets cheaper twice, three outdated heap entries get skipped, and node 5 is never reached.
n = 6, edges = [[0,1,6], [0,2,2], [2,1,3], [2,3,7], [1,3,2], [1,4,9], [3,4,1], [2,4,12]], start = 0
| Step | Pop [d, node] | Prices that improve | dist after | What happens | Why |
|---|---|---|---|---|---|
| 1 | [0, 0] | 1: 6, 2: 2 | [0, 6, 2, ∞, ∞, ∞] | Push [6, 1] and [2, 2] | The start is the only entry. Both of its neighbors had Infinity, so both improve. |
| 2 | [2, 2] | 1: 5, 3: 9, 4: 14 | [0, 5, 2, 9, 14, ∞] | 2 + 3 = 5 beats 6 | Node 2 is the closest unfinished node. Going through it is cheaper for node 1, so a new entry [5, 1] is pushed. The old [6, 1] stays in the heap. |
| 3 | [5, 1] | 3: 7 | [0, 5, 2, 7, 14, ∞] | 5 + 9 = 14 doesn't beat 14 | Node 1 lowers node 3 from 9 to 7. For node 4 the new price ties the old one, and < is strict, so nothing is pushed. |
| 4 | [6, 1] | none | unchanged | 6 > dist[1]: skip | This is the old entry for node 1. Its distance 6 is bigger than the stored 5, so the continue throws it away. |
| 5 | [7, 3] | 4: 8 | [0, 5, 2, 7, 8, ∞] | 7 + 1 = 8 beats 14 | Node 3 is finished at 7, through 0 → 2 → 1 → 3. Its edge to node 4 gives a much cheaper price. |
| 6 | [8, 4] | none | unchanged | Finished, no edges out | Node 4 is finished at 8. It has no outgoing edges, so there is nothing to offer. |
| 7 | [9, 3] | none | unchanged | 9 > dist[3]: skip | An outdated entry for node 3 from when its price was still 9. |
| 8 | [14, 4] | none | unchanged | 14 > dist[4]: skip | The last outdated entry. After this pop the heap is empty and the loop ends. |
It returns [0, 5, 2, 7, 8, Infinity] after 8 pops: 5 finished nodes and 3 skipped entries. Node 5 has no edges coming into it, so it keeps Infinity.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Dijkstra's algorithm | O(e log n) | O(n + e) | Costs are 0 or more and you need the cheapest cost from one start. |
| BFS | O(V + E) | O(V) | Every edge costs the same, so the fewest edges is also the cheapest. |
| 0-1 BFS | O(V + E) | O(V) | Every edge costs either 0 or 1. A double-ended queue replaces the heap. |
| Bellman-Ford | O(n · e) | O(n) | Some edges are negative, or you must detect a loop that keeps getting cheaper. |
Extra memory: O(n + e): the adjacency lists hold every edge, dist holds n numbers, and with lazy skipping the heap can hold one entry per improving edge.
How the steps grow
- Dijkstra's algorithm
O(e log n) - BFS, 0-1 BFS
O(V + E) - Bellman-Ford
O(n · e)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Dijkstra's algorithm | 33 | 9,966 | 20 million |
| BFS | 20 | 2,000 | 2 million |
| 0-1 BFS | 20 | 2,000 | 2 million |
| Bellman-Ford | 100 | 1 million | 1 trillion |
Inputs that need care
5 tricky inputs
- A node no edge leads to, like node 5 in the trace
- It never gets an entry, so
diststaysInfinity. Convert to-1at the end if the problem asks for that. - A start with no outgoing edges
- The first pop finishes the start and offers nothing. It returns
0for the start andInfinityfor everything else. - Edges that cost 0
- They work.
d + 0 < dist[next]is strict, so a tie never pushes a duplicate entry. - A negative edge:
[[0,1,2], [0,2,5], [2,1,-4], [1,3,1]] - This lazy version pushes node 1 again when its price drops, so here it still ends with
[0, 1, 5, 2]. But it loses its speed guarantee, an early stop at target1would return 2 instead of 1, and a negative loop makes it run forever. Use Bellman-Ford. - Two edges between the same pair, with costs 4 and 9
- Both are relaxed. The cheaper one sets the price; the other fails the
<test or leaves an outdated entry that gets skipped.
Variations and follow-ups
Twists interviewers add
Stop at the target
If you only need one target's cost, return d as soon as [d, target] is popped and not skipped. The proof above says that value is final. Stopping when the target is pushed is wrong, because a cheaper route can still turn up.
Several starts at once
To find the distance from the nearest of several sources, such as the closest open store, set each source's dist to 0 and put all of them in the heap at the start. It's one run, not one per source.
Other ways to combine costs
The proof only needs path costs that never go down as a path gets longer. So you can replace d + weight with Math.max(d, weight) to minimize the steepest single step on a route, or work with probabilities by maximizing a product of values between 0 and 1.
Without a heap, for dense graphs
Keep a done array and scan all n nodes for the smallest unfinished dist each round. That's O(n²) total, which beats O(e log n) when nearly every pair of nodes has an edge.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "cheapest", "fastest", "minimum total cost", or "network delay" with costs on the edges, and no costs below zero. If every cost is the same, say so and use BFS instead.
- Explain the key step out loud: "the smallest entry in the heap is final, because every other route would leave through something at least as expensive and only add more". That one sentence covers both why it works and why negative edges break it.
- Test by hand a graph where the direct edge is more expensive than a two-edge detour, like
0 → 1costing 6 against0 → 2 → 1costing 5. Check that the old entry is skipped and doesn't relax edges again. - Expect "how do you get the path, not just the cost?". Answer: keep
prev[next] = nodewheneverdist[next]improves, then walkprevback from the target and reverse the list.
Where frontend code uses it
The same idea in real UI code
- Map and directions features in web apps compute fastest routes with Dijkstra's algorithm or its faster cousin A*.
- Network and dependency dashboards that show the fastest path for a request through a service graph, using measured latencies as edge weights.
- Diagram editors that route connector lines around boxes find the cheapest path on a grid, where turns or crossing cost extra.