Skip to content

Commit fec9e1f

Browse files
committed
toplogical sorting in javascript
1 parent 4796793 commit fec9e1f

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

javascript/toposort.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// topoSort.js
2+
// Topological sort with cycle detection for dependency graphs.
3+
// Accepts either: adjacency-list object or edge list. Throws on cycles.
4+
5+
/**
6+
* @typedef {Record<string,string[]>} Graph
7+
* @typedef {[string,string]} Edge // [from, to]
8+
*/
9+
10+
/**
11+
* Build adjacency list from edges or return copy of given graph.
12+
* @param {Graph|Edge[]} input
13+
* @returns {Graph}
14+
*/
15+
function toGraph(input) {
16+
/** @type {Graph} */
17+
const g = {};
18+
if (Array.isArray(input)) {
19+
for (const [u, v] of input) {
20+
if (!g[u]) g[u] = [];
21+
if (!g[v]) g[v] = [];
22+
g[u].push(v);
23+
}
24+
} else {
25+
for (const k of Object.keys(input)) g[k] = [...(input[k] || [])];
26+
}
27+
// Ensure all nodes appear
28+
for (const k of Object.keys(g)) for (const v of g[k]) if (!g[v]) g[v] = [];
29+
return g;
30+
}
31+
32+
/**
33+
* Topologically sort nodes. Throws Error with cycle nodes if cyclic.
34+
* @param {Graph|Edge[]} input
35+
* @returns {string[]} order
36+
*/
37+
export function topoSort(input) {
38+
const g = toGraph(input);
39+
/** @type {Record<string,0|1|2>} */ // 0=unvisited,1=visiting,2=done
40+
const state = {};
41+
const order = [];
42+
const stack = [];
43+
44+
const visit = (node) => {
45+
const st = state[node] || 0;
46+
if (st === 1) {
47+
// Found a back-edge → cycle is the suffix of the stack up to node
48+
const idx = stack.lastIndexOf(node);
49+
const cycle = stack.slice(idx).concat(node);
50+
const msg = `Cycle detected: ${cycle.join(" -> ")}`;
51+
const err = new Error(msg);
52+
// @ts-ignore attach for debugging
53+
err.cycle = cycle;
54+
throw err;
55+
}
56+
if (st === 2) return;
57+
58+
state[node] = 1;
59+
stack.push(node);
60+
for (const nei of g[node]) visit(nei);
61+
stack.pop();
62+
state[node] = 2;
63+
order.push(node);
64+
};
65+
66+
for (const node of Object.keys(g)) if (!state[node]) visit(node);
67+
return order.reverse();
68+
}
69+
70+
/**
71+
* Group nodes by "level" (distance from sources) using Kahn's algorithm.
72+
* If graph has a cycle, throws like topoSort.
73+
* @param {Graph|Edge[]} input
74+
* @returns {string[][]} levels, where levels[0] are sources
75+
*/
76+
export function topoLevels(input) {
77+
const g = toGraph(input);
78+
const indeg = Object.fromEntries(Object.keys(g).map(k => [k,0]));
79+
for (const u of Object.keys(g)) for (const v of g[u]) indeg[v]++;
80+
81+
/** @type {string[][]} */
82+
const levels = [];
83+
let layer = Object.keys(indeg).filter(k => indeg[k] === 0);
84+
85+
let visited = 0;
86+
while (layer.length) {
87+
levels.push(layer);
88+
const next = [];
89+
for (const u of layer) {
90+
visited++;
91+
for (const v of g[u]) {
92+
if (--indeg[v] === 0) next.push(v);
93+
}
94+
}
95+
layer = next;
96+
}
97+
98+
if (visited !== Object.keys(g).length) {
99+
// cycle present; derive one cycle path using DFS utility
100+
topoSort(g); // will throw with details
101+
}
102+
return levels;
103+
}
104+
105+
// // Example usage:
106+
// // const order = topoSort([["a","b"],["a","c"],["b","d"],["c","d"]]);
107+
// // const levels = topoLevels({ a:["b","c"], b:["d"], c:["d"], d:[] });

0 commit comments

Comments
 (0)