Skip to content

Commit 63cbf62

Browse files
committed
Practice. Add TopologicalSorting
1 parent be2467e commit 63cbf62

File tree

3 files changed

+265
-0
lines changed

3 files changed

+265
-0
lines changed

__tests__/top-sort.spec.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { IGraph } from '../src/practice/graph';
2+
import { isTopSort, pathExists, topSort } from '../src/practice/top-sort';
3+
4+
export const singleNode: IGraph = {
5+
nodes: ['a'],
6+
edges: {},
7+
};
8+
9+
export const loop: IGraph = {
10+
nodes: ['a'],
11+
edges: { a: ['a'] },
12+
};
13+
14+
export const shortChain: IGraph = {
15+
nodes: ['a', 'b'],
16+
edges: { a: ['b'] },
17+
};
18+
19+
export const longChain: IGraph = {
20+
nodes: ['a', 'b', 'c', 'd'],
21+
edges: { a: ['b'], b: ['c'], c: ['d'] },
22+
};
23+
24+
export const linkedChain: IGraph = {
25+
nodes: ['a', 'b', 'c'],
26+
edges: { a: ['b'], b: ['c'], c: ['a'] },
27+
};
28+
29+
describe(pathExists.name, () => {
30+
[
31+
{ graph: singleNode, source: 'a', destination: 'a', expectedResult: false },
32+
33+
{ graph: loop, source: 'a', destination: 'a', expectedResult: true },
34+
35+
{ graph: shortChain, source: 'a', destination: 'b', expectedResult: true },
36+
{ graph: shortChain, source: 'b', destination: 'a', expectedResult: false },
37+
38+
{ graph: longChain, source: 'a', destination: 'b', expectedResult: true },
39+
{ graph: longChain, source: 'b', destination: 'c', expectedResult: true },
40+
{ graph: longChain, source: 'c', destination: 'd', expectedResult: true },
41+
{ graph: longChain, source: 'a', destination: 'c', expectedResult: true },
42+
{ graph: longChain, source: 'b', destination: 'd', expectedResult: true },
43+
{ graph: longChain, source: 'a', destination: 'd', expectedResult: true },
44+
{ graph: longChain, source: 'b', destination: 'a', expectedResult: false },
45+
{ graph: longChain, source: 'c', destination: 'b', expectedResult: false },
46+
{ graph: longChain, source: 'd', destination: 'c', expectedResult: false },
47+
{ graph: longChain, source: 'c', destination: 'a', expectedResult: false },
48+
{ graph: longChain, source: 'd', destination: 'b', expectedResult: false },
49+
{ graph: longChain, source: 'd', destination: 'a', expectedResult: false },
50+
51+
{ graph: linkedChain, source: 'a', destination: 'b', expectedResult: true },
52+
{ graph: linkedChain, source: 'b', destination: 'c', expectedResult: true },
53+
{ graph: linkedChain, source: 'c', destination: 'a', expectedResult: true },
54+
{ graph: linkedChain, source: 'a', destination: 'c', expectedResult: true },
55+
{ graph: linkedChain, source: 'c', destination: 'b', expectedResult: true },
56+
].forEach(({graph, source, destination, expectedResult}) => {
57+
it(`Should return ${expectedResult} for path from ${source} to ${destination} on ${JSON.stringify(graph, null, 2)}`, () => {
58+
expect(pathExists(source, destination, graph)).toEqual(expectedResult);
59+
});
60+
});
61+
});
62+
63+
describe(isTopSort.name, () => {
64+
[
65+
{
66+
graph: singleNode,
67+
orderedNodes: [],
68+
expectedResult: false,
69+
},
70+
{
71+
graph: singleNode,
72+
orderedNodes: ['a'],
73+
expectedResult: true,
74+
},
75+
76+
{
77+
graph: loop,
78+
orderedNodes: ['a'],
79+
expectedResult: false,
80+
},
81+
82+
{
83+
graph: shortChain,
84+
orderedNodes: [],
85+
expectedResult: false,
86+
},
87+
{
88+
graph: shortChain,
89+
orderedNodes: ['a'],
90+
expectedResult: false,
91+
},
92+
{
93+
graph: shortChain,
94+
orderedNodes: ['b'],
95+
expectedResult: false,
96+
},
97+
{
98+
graph: shortChain,
99+
orderedNodes: ['a', 'b'],
100+
expectedResult: true,
101+
},
102+
{
103+
graph: shortChain,
104+
orderedNodes: ['b', 'a'],
105+
expectedResult: false,
106+
},
107+
108+
{
109+
graph: longChain,
110+
orderedNodes: ['a', 'b', 'c', 'd'],
111+
expectedResult: true,
112+
},
113+
{
114+
graph: longChain,
115+
orderedNodes: ['b', 'a', 'c', 'd'],
116+
expectedResult: false,
117+
},
118+
{
119+
graph: longChain,
120+
orderedNodes: ['a', 'c', 'b', 'd'],
121+
expectedResult: false,
122+
},
123+
{
124+
graph: longChain,
125+
orderedNodes: ['a', 'b', 'd', 'c'],
126+
expectedResult: false,
127+
},
128+
129+
{
130+
graph: shortChain,
131+
orderedNodes: ['a', 'b'],
132+
expectedResult: true,
133+
},
134+
{
135+
graph: shortChain,
136+
orderedNodes: ['b', 'a'],
137+
expectedResult: false,
138+
},
139+
140+
{
141+
graph: linkedChain,
142+
orderedNodes: ['a', 'b', 'c', 'd'],
143+
expectedResult: false,
144+
},
145+
{
146+
graph: linkedChain,
147+
orderedNodes: ['b', 'a', 'c', 'd'],
148+
expectedResult: false,
149+
},
150+
{
151+
graph: linkedChain,
152+
orderedNodes: ['a', 'c', 'b', 'd'],
153+
expectedResult: false,
154+
},
155+
{
156+
graph: linkedChain,
157+
orderedNodes: ['a', 'b', 'd', 'c'],
158+
expectedResult: false,
159+
},
160+
].forEach(({graph, orderedNodes, expectedResult}) => {
161+
it(`Should return ${expectedResult} for sequence ${JSON.stringify(orderedNodes)} on ${JSON.stringify(graph, null, 2)}`, () => {
162+
expect(isTopSort(orderedNodes, graph)).toEqual(expectedResult);
163+
});
164+
});
165+
});
166+
167+
describe(topSort.name, () => {
168+
[
169+
singleNode,
170+
shortChain,
171+
longChain,
172+
].forEach(graph => {
173+
it(`Should produce a valid topsort result for graph ${JSON.stringify(graph, null, 2)}`, () => {
174+
const topsortResult = topSort(graph);
175+
expect(isTopSort(topsortResult, graph)).toBeTruthy();
176+
});
177+
});
178+
179+
[
180+
loop,
181+
linkedChain,
182+
].forEach(graph => {
183+
it(`Should throw an error for a cyclic graph ${JSON.stringify(graph, null, 2)}`, () => {
184+
expect(() => topSort(graph)).toThrowError(`Grap has a cycle!`);
185+
});
186+
});
187+
});

src/practice/graph.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface IGraph {
2+
nodes: string[];
3+
edges: { [ source: string ]: string[] };
4+
}

src/practice/top-sort.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { IGraph } from './graph';
2+
3+
export function topSort(
4+
graph: IGraph,
5+
): string[] {
6+
const ordered: string[] = [];
7+
const notVisited = new Set(graph.nodes);
8+
const remainingEdges = Object.keys(graph.edges)
9+
.reduce(
10+
(result, srcNode) => (result[srcNode] = [...graph.edges[srcNode]], result),
11+
{} as { [src: string]: string[] }
12+
);
13+
while (notVisited.size) {
14+
const independentNodes = nodesWithoutIncomingEdges(notVisited, remainingEdges);
15+
if (!independentNodes.size) throw new Error(`Grap has a cycle!`);
16+
17+
ordered.push(...independentNodes);
18+
independentNodes.forEach(node => {
19+
notVisited.delete(node);
20+
delete remainingEdges[node];
21+
});
22+
}
23+
return ordered;
24+
}
25+
26+
export function nodesWithoutIncomingEdges(
27+
notVisited: Set<string>,
28+
remainingEdges: { [ src: string ]: string[] },
29+
): Set<string> {
30+
const copy = new Set(notVisited);
31+
Object
32+
.keys(remainingEdges)
33+
.forEach(src =>
34+
remainingEdges[src]
35+
.forEach(dest => copy.delete(dest))
36+
);
37+
return copy;
38+
}
39+
40+
export function isTopSort(
41+
orderedNodes: string[],
42+
graph: IGraph,
43+
): boolean {
44+
if (graph.nodes.length !== (orderedNodes || []).length) return false;
45+
46+
for (let sourceIndex = 0; sourceIndex < orderedNodes.length; sourceIndex++) {
47+
for (let destinationIndex = sourceIndex; destinationIndex < orderedNodes.length; destinationIndex++) {
48+
const reversePathExists = pathExists(orderedNodes[destinationIndex], orderedNodes[sourceIndex], graph);
49+
if (reversePathExists)
50+
return false;
51+
}
52+
}
53+
return true;
54+
}
55+
56+
export function pathExists(source: string, destination: string, graph: IGraph): boolean {
57+
const toVisit = [source];
58+
const visited = new Set<string | undefined>([]);
59+
60+
while (toVisit.length) {
61+
const currentNode = toVisit.shift();
62+
if (currentNode === undefined) throw new Error(`This should never happen`);
63+
64+
visited.add(currentNode);
65+
const directlyReachable = graph.edges[currentNode] || [];
66+
67+
if (directlyReachable.indexOf(destination) >= 0) return true;
68+
69+
toVisit.push(
70+
...directlyReachable.filter(node => !visited.has(node))
71+
);
72+
}
73+
return false;
74+
}

0 commit comments

Comments
 (0)