Skip to content

Commit 704d9bf

Browse files
authored
Merge pull request #42 from notmatthancock/enh/topo-sort-ensure-no-cycles
Add CycleError option to DFS
2 parents 7c4d36e + 560ba26 commit 704d9bf

5 files changed

Lines changed: 128 additions & 7 deletions

File tree

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,20 @@ Deserializes the given serialized graph. Returns *graph* to support method chain
201201

202202
### Graph Algorithms
203203

204-
<a name="dfs" href="#dfs">#</a> <i>graph</i>.<b>depthFirstSearch</b>([<i>sourceNodes</i>][, <i>includeSourceNodes</i>])
204+
<a name="dfs" href="#dfs">#</a> <i>graph</i>.<b>depthFirstSearch</b>([<i>sourceNodes</i>][, <i>includeSourceNodes</i>][, <i>errorOnCycle</i>])
205205

206206
Performs [Depth-first Search](https://en.wikipedia.org/wiki/Depth-first_search). Returns an array of node identifier strings. The returned array includes nodes visited by the algorithm in the order in which they were visited. Implementation inspired by pseudocode from Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 604.
207207

208208
Arguments:
209209

210210
* *sourceNodes* (optional) - An array of node identifier strings. This specifies the subset of nodes to use as the sources of the depth-first search. If *sourceNodes* is not specified, all **[nodes](#nodes)** in the graph are used as source nodes.
211211
* *includeSourceNodes* (optional) - A boolean specifying whether or not to include the source nodes in the returned array. If *includeSourceNodes* is not specified, it is treated as `true` (all source nodes are included in the returned array).
212+
* *errorOnCycle* (optional) - A boolean indicating that a `CycleError` should be thrown whenever a cycle is first encountered. Defaults to `false`.
213+
214+
215+
<a name="has-cycle" href="#has-cycle">#</a> <i>graph</i>.<b>hasCycle</b>()
216+
217+
Checks if the graph has any cycles. Returns `true` if it does and `false` otherwise.
212218

213219
<a name="lca" href="#lca">#</a> <i>graph</i>.<b>lowestCommonAncestors</b>([<i>node1</i>][, <i>node2</i>])
214220

@@ -225,6 +231,8 @@ Performs [Topological Sort](https://en.wikipedia.org/wiki/Topological_sorting).
225231

226232
See **[depthFirstSearch](#dfs)** for documentation of the arguments *sourceNodes* and *includeSourceNodes*.
227233

234+
Note: this function raises a `CycleError` when the input is not a DAG.
235+
228236
<a name="shortest-path" href="#shortest-path">#</a> <i>graph</i>.<b>shortestPath</b>(<i>sourceNode</i>, <i>destinationNode</i>)
229237

230238
Performs [Dijkstras Algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm). Returns an array of node identifier strings. The returned array includes the nodes of the shortest path from source to destination node. The returned array also contains a `weight` property, which is the total weight over all edges in the path. Inspired by by Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 658.
@@ -234,4 +242,3 @@ Performs [Dijkstras Algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algori
234242
<img src="https://cloud.githubusercontent.com/assets/68416/15298394/a7a0a66a-1bbc-11e6-9636-367bed9165fc.png">
235243
</a>
236244
</p>
237-

index.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ declare function Graph(serialized?: Serialized): {
2121
getEdgeWeight: (u: NodeId, v: NodeId) => EdgeWeight;
2222
indegree: (node: NodeId) => number;
2323
outdegree: (node: NodeId) => number;
24-
depthFirstSearch: (sourceNodes?: string[] | undefined, includeSourceNodes?: boolean) => string[];
24+
depthFirstSearch: (sourceNodes?: string[] | undefined, includeSourceNodes?: boolean, errorOnCycle?: boolean) => string[];
25+
hasCycle: () => boolean;
2526
lowestCommonAncestors: (node1: NodeId, node2: NodeId) => string[];
2627
topologicalSort: (sourceNodes: NodeId[], includeSourceNodes?: boolean) => string[];
2728
shortestPath: (source: NodeId, destination: NodeId) => string[] & {

index.js

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
11
"use strict";
2+
var __extends = (this && this.__extends) || (function () {
3+
var extendStatics = function (d, b) {
4+
extendStatics = Object.setPrototypeOf ||
5+
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6+
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
7+
return extendStatics(d, b);
8+
};
9+
return function (d, b) {
10+
extendStatics(d, b);
11+
function __() { this.constructor = d; }
12+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13+
};
14+
})();
15+
var CycleError = /** @class */ (function (_super) {
16+
__extends(CycleError, _super);
17+
function CycleError(message) {
18+
var _this = _super.call(this, message) || this;
19+
Object.setPrototypeOf(_this, CycleError.prototype);
20+
return _this;
21+
}
22+
return CycleError;
23+
}(Error));
224
// A graph data structure with depth-first search and topological sort.
325
function Graph(serialized) {
426
// Returned graph instance
@@ -14,6 +36,7 @@ function Graph(serialized) {
1436
indegree: indegree,
1537
outdegree: outdegree,
1638
depthFirstSearch: depthFirstSearch,
39+
hasCycle: hasCycle,
1740
lowestCommonAncestors: lowestCommonAncestors,
1841
topologicalSort: topologicalSort,
1942
shortestPath: shortestPath,
@@ -133,20 +156,27 @@ function Graph(serialized) {
133156
// include or exclude the source nodes from the result (true by default).
134157
// If `sourceNodes` is not specified, all nodes in the graph
135158
// are used as source nodes.
136-
function depthFirstSearch(sourceNodes, includeSourceNodes) {
159+
function depthFirstSearch(sourceNodes, includeSourceNodes, errorOnCycle) {
137160
if (includeSourceNodes === void 0) { includeSourceNodes = true; }
161+
if (errorOnCycle === void 0) { errorOnCycle = false; }
138162
if (!sourceNodes) {
139163
sourceNodes = nodes();
140164
}
141165
if (typeof includeSourceNodes !== "boolean") {
142166
includeSourceNodes = true;
143167
}
144168
var visited = {};
169+
var visiting = {};
145170
var nodeList = [];
146171
function DFSVisit(node) {
172+
if (visiting[node] && errorOnCycle) {
173+
throw new CycleError("Cycle found");
174+
}
147175
if (!visited[node]) {
148176
visited[node] = true;
177+
visiting[node] = true; // temporary flag while visiting
149178
adjacent(node).forEach(DFSVisit);
179+
visiting[node] = false;
150180
nodeList.push(node);
151181
}
152182
}
@@ -163,6 +193,22 @@ function Graph(serialized) {
163193
}
164194
return nodeList;
165195
}
196+
// Returns true if the graph has one or more cycles and false otherwise
197+
function hasCycle() {
198+
try {
199+
depthFirstSearch(undefined, true, true);
200+
// No error thrown -> no cycles
201+
return false;
202+
}
203+
catch (error) {
204+
if (error instanceof CycleError) {
205+
return true;
206+
}
207+
else {
208+
throw error;
209+
}
210+
}
211+
}
166212
// Least Common Ancestors
167213
// Inspired by https://github.com/relaxedws/lca/blob/master/src/LowestCommonAncestor.php code
168214
// but uses depth search instead of breadth. Also uses some optimizations
@@ -210,7 +256,7 @@ function Graph(serialized) {
210256
// Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 613
211257
function topologicalSort(sourceNodes, includeSourceNodes) {
212258
if (includeSourceNodes === void 0) { includeSourceNodes = true; }
213-
return depthFirstSearch(sourceNodes, includeSourceNodes).reverse();
259+
return depthFirstSearch(sourceNodes, includeSourceNodes, true).reverse();
214260
}
215261
// Dijkstra's Shortest Path Algorithm.
216262
// Cormen et al. "Introduction to Algorithms" 3rd Ed. p. 658

index.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ interface Serialized {
77
links: { source: NodeId; target: NodeId; weight: EdgeWeight }[];
88
}
99

10+
class CycleError extends Error {
11+
constructor(message: string) {
12+
super(message);
13+
Object.setPrototypeOf(this, CycleError.prototype);
14+
}
15+
}
16+
1017
// A graph data structure with depth-first search and topological sort.
1118
function Graph(serialized?: Serialized) {
1219
// Returned graph instance
@@ -22,6 +29,7 @@ function Graph(serialized?: Serialized) {
2229
indegree,
2330
outdegree,
2431
depthFirstSearch,
32+
hasCycle,
2533
lowestCommonAncestors,
2634
topologicalSort,
2735
shortestPath,
@@ -163,7 +171,8 @@ function Graph(serialized?: Serialized) {
163171
// are used as source nodes.
164172
function depthFirstSearch(
165173
sourceNodes?: NodeId[],
166-
includeSourceNodes: boolean = true
174+
includeSourceNodes: boolean = true,
175+
errorOnCycle: boolean = false,
167176
) {
168177
if (!sourceNodes) {
169178
sourceNodes = nodes();
@@ -174,12 +183,18 @@ function Graph(serialized?: Serialized) {
174183
}
175184

176185
const visited: Record<NodeId, boolean> = {};
186+
const visiting: Record<NodeId, boolean> = {};
177187
const nodeList: NodeId[] = [];
178188

179189
function DFSVisit(node: NodeId) {
190+
if (visiting[node] && errorOnCycle) {
191+
throw new CycleError("Cycle found");
192+
}
180193
if (!visited[node]) {
181194
visited[node] = true;
195+
visiting[node] = true; // temporary flag while visiting
182196
adjacent(node).forEach(DFSVisit);
197+
visiting[node] = false;
183198
nodeList.push(node);
184199
}
185200
}
@@ -198,6 +213,23 @@ function Graph(serialized?: Serialized) {
198213
return nodeList;
199214
}
200215

216+
// Returns true if the graph has one or more cycles and false otherwise
217+
function hasCycle(): boolean {
218+
try {
219+
depthFirstSearch(undefined, true, true);
220+
// No error thrown -> no cycles
221+
return false;
222+
}
223+
catch (error) {
224+
if (error instanceof CycleError) {
225+
return true;
226+
}
227+
else {
228+
throw error;
229+
}
230+
}
231+
}
232+
201233
// Least Common Ancestors
202234
// Inspired by https://github.com/relaxedws/lca/blob/master/src/LowestCommonAncestor.php code
203235
// but uses depth search instead of breadth. Also uses some optimizations
@@ -253,7 +285,7 @@ function Graph(serialized?: Serialized) {
253285
sourceNodes: NodeId[],
254286
includeSourceNodes: boolean = true
255287
) {
256-
return depthFirstSearch(sourceNodes, includeSourceNodes).reverse();
288+
return depthFirstSearch(sourceNodes, includeSourceNodes, true).reverse();
257289
}
258290

259291
// Dijkstra's Shortest Path Algorithm.

test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,34 @@ describe("Graph", function() {
148148

149149
describe("Algorithms", function() {
150150

151+
it("Should detect cycle.", function() {
152+
var graph = Graph();
153+
graph.addEdge("a", "b");
154+
graph.addEdge("b", "a");
155+
assert(graph.hasCycle());
156+
});
157+
158+
it("Should detect cycle (long).", function() {
159+
var graph = Graph();
160+
graph.addEdge("a", "b");
161+
graph.addEdge("b", "c");
162+
graph.addEdge("c", "d");
163+
graph.addEdge("d", "a");
164+
assert(graph.hasCycle());
165+
});
166+
167+
it("Should detect cycle (loop).", function() {
168+
var graph = Graph();
169+
graph.addEdge("a", "a");
170+
assert(graph.hasCycle());
171+
});
172+
173+
it("Should not detect cycle.", function() {
174+
var graph = Graph();
175+
graph.addEdge("a", "b");
176+
assert(!graph.hasCycle());
177+
});
178+
151179
// This example is from Cormen et al. "Introduction to Algorithms" page 550
152180
it("Should compute topological sort.", function (){
153181

@@ -246,6 +274,13 @@ describe("Graph", function() {
246274
output(graph, "cycles");
247275
});
248276

277+
it("Should error on non-DAG topological sort", function() {
278+
var graph = Graph();
279+
graph.addEdge("a", "b");
280+
graph.addEdge("b", "a");
281+
assert.throws(graph.topologicalSort);
282+
});
283+
249284
it("Should compute lowest common ancestors.", function (){
250285
var graph = Graph()
251286

0 commit comments

Comments
 (0)