diff --git a/README.md b/README.md index 4bad6da..cb6f125 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,16 @@ declare const isCyclic: (graph: Graph) => boolean; Returns `true` if the graph provided contains any cycles (this includes "loops" — an edge that starts and ends at the same vertex), otherwise returns `false`. +### topologicalSort + +```ts +declare const topologicalSort: (graph: Graph) => Array; +``` + +Given a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph), returns an +array of the graph's vertices sorted using a +[topological sort](https://en.wikipedia.org/wiki/Topological_sorting). + ### fromD3 ```ts @@ -128,10 +138,9 @@ This representation of a graph is convinient for using with ## Roadmap -1. Topological sort function. -2. Allow arbitrary data associated with nodes and links that can be preserved +1. Allow arbitrary data associated with nodes and links that can be preserved when converting between `Graph` and `D3Graph` objects. -3. Better docs with visualisations to explain the concepts for anyone not +2. Better docs with visualisations to explain the concepts for anyone not familiar with graph theory. -4. Edge weights. -5. More helpful functions! +3. Edge weights. +4. More helpful functions! diff --git a/index.js.flow b/index.js.flow index 2a7caa9..53ef5db 100644 --- a/index.js.flow +++ b/index.js.flow @@ -16,3 +16,4 @@ declare export type D3Graph = { declare export function toD3(graph: Graph): D3Graph; declare export function fromD3(graph: D3Graph): Graph; declare export function isCyclic(graph: Graph): boolean; +declare export function topologicalSort(graph: Graph): Array; diff --git a/index.ts b/index.ts index 923174a..d0d7978 100644 --- a/index.ts +++ b/index.ts @@ -103,4 +103,49 @@ const isCyclic = (graph: Graph): boolean => { return false; }; -export { Graph, D3Graph, isCyclic, fromD3, toD3 }; +const getInDegrees = (graph: Graph): Array => { + const result: Array = []; + for (let i = 0; i < graph.size; i++) { + result[i] = 0; + } + for (let u = 0; u < graph.size; u++) { + for (let v = 0; v < graph.size; v++) { + result[v] += graph.adjacencyMatrix[u][v]; + } + } + return result; +}; + +const topologicalSort = (graph: Graph): Array => { + if (isCyclic(graph)) throw "Cannot sort a graph that contains cycles."; + + const result: Array = []; + const visited: Set = new Set(); + const queue: Array = []; + const inDegree = getInDegrees(graph); + + for (let i = 0; i < graph.size; i++) { + if (inDegree[i] === 0) { + queue.push(i); + visited.add(i); + } + } + + while (queue.length > 0) { + const v = queue.shift(); + result.push(v); + for (let i = 0; i < graph.size; i++) { + if (graph.adjacencyMatrix[v][i] > 0 && !visited.has(i)) { + inDegree[i] -= graph.adjacencyMatrix[v][i]; + if (inDegree[i] <= 0) { + queue.push(i); + visited.add(i); + } + } + } + } + + return result; +}; + +export { Graph, D3Graph, isCyclic, topologicalSort, fromD3, toD3 }; diff --git a/test.ts b/test.ts index 55e6485..1088d25 100644 --- a/test.ts +++ b/test.ts @@ -1,4 +1,4 @@ -import { fromD3, isCyclic, toD3 } from "./index"; +import { fromD3, isCyclic, toD3, topologicalSort } from "./index"; import { strict as assert } from "assert"; @@ -86,6 +86,71 @@ assert.equal( true ); +assert.deepEqual( + topologicalSort({ + size: 1, + adjacencyMatrix: [[0]], + }), + [0] +); + +assert.deepEqual( + topologicalSort({ + size: 3, + adjacencyMatrix: [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ], + }), + [0, 1, 2] +); + +assert.deepEqual( + topologicalSort({ + size: 3, + adjacencyMatrix: [ + [0, 1, 0], + [0, 0, 0], + [1, 0, 0], + ], + }), + [2, 0, 1] +); + +assert.deepEqual( + topologicalSort({ + size: 3, + adjacencyMatrix: [ + [0, 0, 0], + [1, 0, 0], + [1, 0, 0], + ], + }), + [1, 2, 0] +); + +assert.deepEqual( + topologicalSort({ + size: 2, + adjacencyMatrix: [ + [0, 0], + [10, 0], + ], + }), + [1, 0] +); + +assert.throws(() => { + topologicalSort({ + size: 2, + adjacencyMatrix: [ + [0, 1], + [1, 0], + ], + }); +}); + assert.deepEqual( fromD3({ nodes: [{ id: "a" }, { id: "b" }, { id: "c" }],