Skip to content

Commit 8142763

Browse files
committed
more tests; fixed an infinite loop when no path found
1 parent 1f5995f commit 8142763

File tree

4 files changed

+118
-45
lines changed

4 files changed

+118
-45
lines changed

README.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
# Dijkstra's Shortest Path Algorithm
22

3-
A pretty good implementation of Dijkstra's shortest-path algorithm.
3+
A pretty good implementation of Dijkstra's shortest-path algorithm for Deno.
44

5-
This implementation is designed to process large in-memory graphs. It will
6-
perform reasonably well even when the number of edges is in the millions. Using
7-
numbered indexes for nodes, rather than string labels or objects, was by design.
8-
This reduced the memory footprint needed for the graph and speeds up graph
9-
creation and lookups.
5+
This implementation is able to process large in-memory graphs. It will
6+
perform reasonably well even when the number of edges is in the millions.
107

118
This code was adapted from Typescript/Deno from
129
[A Walkthrough of Dijkstra's Algorithm (In JavaScript!)](https://medium.com/@adriennetjohnson/a-walkthrough-of-dijkstras-algorithm-in-javascript-e94b74192026)

dijkstra.example.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { DijkstraShortestPathSolver } from "./dijkstra.ts";
2+
import { assertEquals } from "./deps.ts";
3+
4+
const FULLSTACK = 0;
5+
const DIGINN = 1;
6+
const DUBLINER = 2;
7+
const STARBUCKS = 3;
8+
const CAFEGRUMPY = 4;
9+
const INSOMNIACOOKIES = 5;
10+
11+
const cafes = DijkstraShortestPathSolver.init(6);
12+
13+
cafes.addBidirEdge(DIGINN, FULLSTACK, 7);
14+
cafes.addBidirEdge(FULLSTACK, STARBUCKS, 6);
15+
cafes.addBidirEdge(DIGINN, DUBLINER, 4);
16+
cafes.addBidirEdge(FULLSTACK, DUBLINER, 2);
17+
cafes.addBidirEdge(DUBLINER, STARBUCKS, 3);
18+
cafes.addBidirEdge(DIGINN, CAFEGRUMPY, 9);
19+
cafes.addBidirEdge(CAFEGRUMPY, INSOMNIACOOKIES, 5);
20+
cafes.addBidirEdge(DUBLINER, INSOMNIACOOKIES, 7);
21+
cafes.addBidirEdge(STARBUCKS, INSOMNIACOOKIES, 6);
22+
23+
Deno.test("demonstrate finding shortest path", () => {
24+
const path = cafes.calculateFor(FULLSTACK).shortestPathTo(CAFEGRUMPY);
25+
assertEquals(
26+
path,
27+
[FULLSTACK, DUBLINER, INSOMNIACOOKIES, CAFEGRUMPY],
28+
"shortest path from FULLSTACK to CAFEGRUMPY",
29+
);
30+
});
31+
32+
Deno.test("demonstrate finding the weight of the shortest path", () => {
33+
assertEquals(
34+
cafes.calculateFor(FULLSTACK).totalWeight(CAFEGRUMPY),
35+
14,
36+
"weight of the shortest path",
37+
);
38+
});

dijkstra.test.ts

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,31 @@
11
import { DijkstraShortestPathSolver } from "./dijkstra.ts";
2-
import { assertEquals } from "./deps.ts";
2+
import { assertEquals, assertThrows } from "./deps.ts";
33

4-
const FULLSTACK = 0;
5-
const DIGINN = 1;
6-
const DUBLINER = 2;
7-
const STARBUCKS = 3;
8-
const CAFEGRUMPY = 4;
9-
const INSOMNIACOOKIES = 5;
4+
Deno.test("graph contains at least 2 nodes", () => {
5+
assertThrows(() => DijkstraShortestPathSolver.init(1), RangeError);
6+
});
107

11-
const cafes = DijkstraShortestPathSolver.init(6);
8+
Deno.test("throws if no path found", () => {
9+
const solver = DijkstraShortestPathSolver.init(2);
10+
const shortestPath = solver.calculateFor(0);
11+
assertThrows(() => shortestPath.shortestPathTo(1), Error);
12+
});
1213

13-
cafes.addBidirEdge(DIGINN, FULLSTACK, 7);
14-
cafes.addBidirEdge(FULLSTACK, STARBUCKS, 6);
15-
cafes.addBidirEdge(DIGINN, DUBLINER, 4);
16-
cafes.addBidirEdge(FULLSTACK, DUBLINER, 2);
17-
cafes.addBidirEdge(DUBLINER, STARBUCKS, 3);
18-
cafes.addBidirEdge(DIGINN, CAFEGRUMPY, 9);
19-
cafes.addBidirEdge(CAFEGRUMPY, INSOMNIACOOKIES, 5);
20-
cafes.addBidirEdge(DUBLINER, INSOMNIACOOKIES, 7);
21-
cafes.addBidirEdge(STARBUCKS, INSOMNIACOOKIES, 6);
14+
Deno.test("some paths are valid, others are not", () => {
15+
const solver = DijkstraShortestPathSolver.init(3);
16+
solver.addEdge(0, 2, 42);
2217

23-
Deno.test("demonstrate finding shortest path", () => {
24-
const path = cafes.calculateFor(FULLSTACK).shortestPathTo(CAFEGRUMPY);
25-
assertEquals(
26-
path,
27-
[FULLSTACK, DUBLINER, INSOMNIACOOKIES, CAFEGRUMPY],
28-
"shortest path from FULLSTACK to CAFEGRUMPY",
29-
);
18+
const shortestPath = solver.calculateFor(0);
19+
assertThrows(() => shortestPath.shortestPathTo(1), Error);
20+
assertEquals(shortestPath.shortestPathTo(2), [0, 2]);
21+
assertEquals(shortestPath.totalWeight(2), 42);
3022
});
3123

32-
Deno.test("demonstrate finding the weight of the shortest path", () => {
33-
assertEquals(
34-
cafes.calculateFor(FULLSTACK).weights[CAFEGRUMPY],
35-
14,
36-
"weight of the shortest path",
37-
);
24+
Deno.test("finds shortest path between two nodes", () => {
25+
const solver = DijkstraShortestPathSolver.init(2);
26+
solver.addEdge(0, 1, 1);
27+
28+
const shortestPath = solver.calculateFor(0);
29+
assertEquals(shortestPath.shortestPathTo(1), [0, 1]);
30+
assertEquals(shortestPath.totalWeight(1), 1);
3831
});

dijkstra.ts

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ interface IPath {
2222
*
2323
* Nodes are numbered from 0 to n-1.
2424
*
25+
* This supports having a template graph and adding variable nodes for processing with {@link DijkstraShortestPathSolver.clone}
26+
* and {@link DijkstraShortestPathSolver.addNode}. This operations are additive. There is not a good general way to remove nodes
27+
* and edges (subtractive) once they have been added.
28+
*
2529
* Adapted from https://medium.com/@adriennetjohnson/a-walkthrough-of-dijkstras-algorithm-in-javascript-e94b74192026
2630
* This has been made much faster by treating nodes as an index rather than a string (name). We use `tinyqueue`
2731
* as our priority queue. All map-likes have been eliminated, but there are still object references. So this is
@@ -39,14 +43,23 @@ export class DijkstraShortestPathSolver {
3943
* @returns A new solver.
4044
*/
4145
static init(nodes: number): DijkstraShortestPathSolver {
46+
if (nodes < 2) {
47+
throw new RangeError(`solver requires at least 2 nodes: ${nodes}`);
48+
}
49+
4250
return new DijkstraShortestPathSolver(
4351
new Array(nodes).fill(null).map((_v) => new Array(0)),
4452
);
4553
}
4654

4755
/**
4856
* A clone of this solver.
57+
*
58+
* This clone operation is generally very fast, as it is based on array shallow copies.
59+
*
4960
* @returns A cloned solver.
61+
*
62+
* @see {@link DijkstraShortestPathSolver.addNode}
5063
*/
5164
clone(): DijkstraShortestPathSolver {
5265
return new DijkstraShortestPathSolver(
@@ -61,6 +74,23 @@ export class DijkstraShortestPathSolver {
6174
return this.adjacencyList.length;
6275
}
6376

77+
/**
78+
* Add a new node to the graph.
79+
*
80+
* The typical use case for this is when you have a static graph and you need to add a small number
81+
* of additional nodes and edges prior to each compute. You would clone the solver, add your variable
82+
* nodes and edges, and then solve, saving you the time of having to recreate the whole graph from
83+
* scratch each time.
84+
*
85+
* @returns The index of the new node.
86+
*
87+
* @see {@link DijkstraShortestPathSolver.clone}
88+
*/
89+
addNode(): number {
90+
this.adjacencyList.push(new Array(0));
91+
return this.nodes - 1;
92+
}
93+
6494
/**
6595
* Add an edge (in one direction).
6696
* @param fromNode Starting node.
@@ -135,23 +165,21 @@ export class DijkstraShortestPathSolver {
135165

136166
while (pq.length !== 0) {
137167
const shortestStep = pq.pop();
138-
if (shortestStep === undefined) {
139-
throw new Error("shortest-step undefined");
140-
}
141-
const currentNode = shortestStep.toNode;
142168

143-
this.adjacencyList[currentNode].forEach((neighbor) => {
169+
const currentNode = shortestStep!.toNode;
170+
171+
for (const neighbor of this.adjacencyList[currentNode]) {
144172
const weight = weights[currentNode] + neighbor.weight;
145173

146174
if (weight < weights[neighbor.toNode]) {
147175
weights[neighbor.toNode] = weight;
148176
backtrace[neighbor.toNode] = currentNode;
149177
pq.push({ toNode: neighbor.toNode, weight: weight });
150178
}
151-
});
179+
}
152180
}
153181

154-
return new ShortestPaths(startNode, backtrace, weights);
182+
return new ShortestPaths(this.nodes, startNode, backtrace, weights);
155183
}
156184
}
157185

@@ -160,22 +188,33 @@ export class DijkstraShortestPathSolver {
160188
*/
161189
export class ShortestPaths {
162190
constructor(
191+
public readonly nodes: number,
163192
public readonly startNode: number,
164-
public readonly backtrace: number[],
165-
public readonly weights: number[],
193+
private readonly backtrace: number[],
194+
private readonly weights: number[],
166195
) {}
167196

168197
/**
169198
* Find the shortest path to the given end node.
170199
* @param endNode The end node.
200+
* @throws {@link Error} No path found.
171201
*/
172202
shortestPathTo(endNode: number): number[] {
203+
if (endNode < 0 || endNode >= this.nodes) {
204+
throw new RangeError(
205+
`end-node must be in range 0 to ${this.nodes - 1}: ${endNode}`,
206+
);
207+
}
208+
173209
const path = [endNode];
174210
let lastStep = endNode;
175211

176212
while (lastStep != this.startNode) {
177213
path.unshift(this.backtrace[lastStep]);
178214
lastStep = this.backtrace[lastStep];
215+
if (lastStep === undefined) {
216+
throw new Error(`no path from ${this.startNode} to ${endNode}`);
217+
}
179218
}
180219

181220
return path;
@@ -186,6 +225,12 @@ export class ShortestPaths {
186225
* @param endNode The end node.
187226
*/
188227
totalWeight(endNode: number): number {
228+
if (endNode < 0 || endNode >= this.nodes) {
229+
throw new RangeError(
230+
`end-node must be in range 0 to ${this.nodes - 1}: ${endNode}`,
231+
);
232+
}
233+
189234
return this.weights[endNode];
190235
}
191236
}

0 commit comments

Comments
 (0)