Skip to content

Commit b4450e5

Browse files
committed
Cache topological sort for preformance (no incremental update to the sort)
1 parent 39cf867 commit b4450e5

File tree

2 files changed

+31
-1
lines changed

2 files changed

+31
-1
lines changed

src/directedAcyclicGraph.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export class CycleError<T> extends Error {
1010
}
1111
}
1212
export default class DirectedAcyclicGraph<T> extends DirectedGraph<T> {
13+
private _topologicallySortedNodes?: Array<T>;
14+
1315
static fromDirectedGraph<T>(graph: DirectedGraph<T>): DirectedAcyclicGraph<T> {
1416
if (!graph.isAcyclic()) {
1517
throw new CycleError("Can't convert that graph to a DAG because it contains a cycle")
@@ -27,11 +29,26 @@ export default class DirectedAcyclicGraph<T> extends DirectedGraph<T> {
2729
if (this.wouldAddingEdgeCreateCyle(node1Identity, node2Identity)) {
2830
throw new CycleError(`Can't add edge from ${node1Identity} to ${node2Identity} it would create a cycle`)
2931
}
32+
33+
// Invalidate cache of toposorted nodes
34+
this._topologicallySortedNodes = undefined;
3035
super.addEdge(node1Identity, node2Identity, true)
3136
}
3237

38+
// Maintain topo sort when inserting node
39+
insert(node: T): string {
40+
if (this._topologicallySortedNodes)
41+
this._topologicallySortedNodes = [node, ...this._topologicallySortedNodes];
42+
43+
return super.insert(node)
44+
}
45+
3346
// This is an implementation of Kahn's algorithim
3447
topologicallySortedNodes(): Array<T> {
48+
if (this._topologicallySortedNodes != undefined) {
49+
return this._topologicallySortedNodes;
50+
}
51+
3552
const nodeIndices = Array.from(this.nodes.keys());
3653
const nodeInDegrees = new Map(Array.from(this.nodes.keys()).map(n => [n, this.inDegreeOfNode(n)]))
3754

@@ -40,7 +57,9 @@ export default class DirectedAcyclicGraph<T> extends DirectedGraph<T> {
4057
let toSearch = Array.from(nodeInDegrees).filter(pair => pair[1] === 0)
4158

4259
if (toSearch.length === this.nodes.size) {
43-
return Array.from(this.nodes.values())
60+
const arrayOfNodes = Array.from(this.nodes.values());
61+
this._topologicallySortedNodes = arrayOfNodes
62+
return arrayOfNodes
4463
}
4564

4665
let toReturn: Array<T> = []
@@ -63,6 +82,9 @@ export default class DirectedAcyclicGraph<T> extends DirectedGraph<T> {
6382
})
6483
}
6584

85+
// Update cache
86+
this._topologicallySortedNodes = toReturn;
87+
6688
// we shouldn't need to account for the error case of there being a cycle because it shouldn't
6789
// be possible to instantiate this class in a state (or put it in a state) where there is a cycle.
6890

test/directedAcyclicGraph.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ describe("Directed Acyclic Graph", () => {
8181

8282
expect([{ name: 'C' }, { name: 'D' }]).toContainEqual(topoList3[1])
8383
expect([{ name: 'C' }, { name: 'D' }]).toContainEqual(topoList3[2])
84+
85+
graph.insert({ name: 'F' })
86+
87+
const topoList4 = graph.topologicallySortedNodes();
88+
89+
expect(topoList4).toContainEqual({ name: 'F' })
90+
expect([{ name: 'A' }, { name: 'F' }]).toContainEqual(topoList4[0])
91+
expect([{ name: 'A' }, { name: 'F' }]).toContainEqual(topoList4[1])
8492
})
8593

8694
})

0 commit comments

Comments
 (0)