From e73dc2dfd7c8fb31723b74c6587181ae9f22bddb Mon Sep 17 00:00:00 2001 From: Oleksii Trekhleb Date: Tue, 8 May 2018 19:27:42 +0300 Subject: [PATCH] Add topological sorting. --- README.md | 2 +- .../graph/topological-sorting/README.md | 55 +++++++++++++++++++ .../__test__/topologicalSort.test.js | 53 ++++++++++++++++++ .../topological-sorting/topologicalSort.js | 47 ++++++++++++++++ src/data-structures/linked-list/LinkedList.js | 31 +++++++++++ src/data-structures/stack/Stack.js | 26 +++++++++ .../stack/__test__/Stack.test.js | 12 ++++ 7 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 src/algorithms/graph/topological-sorting/README.md create mode 100644 src/algorithms/graph/topological-sorting/__test__/topologicalSort.test.js create mode 100644 src/algorithms/graph/topological-sorting/topologicalSort.js diff --git a/README.md b/README.md index 73e23a13..baef23fa 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ * [Detect Cycle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/detect-cycle) - for both directed and undirected graphs (DFS and Disjoint Set based versions) * [Prim’s Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/prim) - finding Minimum Spanning Tree (MST) for weighted undirected graph * [Kruskal’s Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/kruskal) - finding Minimum Spanning Tree (MST) for weighted undirected graph - * Topological Sorting + * [Topological Sorting](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/topological-sorting) - DFS method * Eulerian path, Eulerian circuit * Strongly Connected Component algorithm * Shortest Path Faster Algorithm (SPFA) diff --git a/src/algorithms/graph/topological-sorting/README.md b/src/algorithms/graph/topological-sorting/README.md new file mode 100644 index 00000000..c911c1ca --- /dev/null +++ b/src/algorithms/graph/topological-sorting/README.md @@ -0,0 +1,55 @@ +# Topological Sorting + +In the field of computer science, a topological sort or +topological ordering of a directed graph is a linear ordering +of its vertices such that for every directed edge `uv` from +vertex `u` to vertex `v`, `u` comes before `v` in the ordering. + +For instance, the vertices of the graph may represent tasks to +be performed, and the edges may represent constraints that one +task must be performed before another; in this application, a +topological ordering is just a valid sequence for the tasks. + +A topological ordering is possible if and only if the graph has +no directed cycles, that is, if it is a [directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph) +(DAG). Any DAG has at least one topological ordering, and algorithms are +known for constructing a topological ordering of any DAG in linear time. + +![Directed Acyclic Graph](https://upload.wikimedia.org/wikipedia/commons/c/c6/Topological_Ordering.svg) + +A topological ordering of a directed acyclic graph: every edge goes from +earlier in the ordering (upper left) to later in the ordering (lower right). +A directed graph is acyclic if and only if it has a topological ordering. + +## Example + +![Topologic Sorting](https://upload.wikimedia.org/wikipedia/commons/0/03/Directed_acyclic_graph_2.svg) + +The graph shown above has many valid topological sorts, including: + +- `5, 7, 3, 11, 8, 2, 9, 10` (visual left-to-right, top-to-bottom) +- `3, 5, 7, 8, 11, 2, 9, 10` (smallest-numbered available vertex first) +- `5, 7, 3, 8, 11, 10, 9, 2` (fewest edges first) +- `7, 5, 11, 3, 10, 8, 9, 2` (largest-numbered available vertex first) +- `5, 7, 11, 2, 3, 8, 9, 10` (attempting top-to-bottom, left-to-right) +- `3, 7, 8, 5, 11, 10, 2, 9` (arbitrary) + +## Application + +The canonical application of topological sorting is in +**scheduling a sequence of jobs** or tasks based on their dependencies. The jobs +are represented by vertices, and there is an edge from `x` to `y` if +job `x` must be completed before job `y` can be started (for +example, when washing clothes, the washing machine must finish +before we put the clothes in the dryer). Then, a topological sort +gives an order in which to perform the jobs. + +Other application is **dependency resolution**. Each vertex is a package +and each edge is a dependency of package `a` on package 'b'. Then topological +sorting will provide a sequence of installing dependencies in a way that every +next dependency has its dependent packages to be installed in prior. + +## References + +- [Wikipedia](https://en.wikipedia.org/wiki/Topological_sorting) +- [Topological Sorting on YouTube by Tushar Roy](https://www.youtube.com/watch?v=ddTC4Zovtbc) diff --git a/src/algorithms/graph/topological-sorting/__test__/topologicalSort.test.js b/src/algorithms/graph/topological-sorting/__test__/topologicalSort.test.js new file mode 100644 index 00000000..e2904cfb --- /dev/null +++ b/src/algorithms/graph/topological-sorting/__test__/topologicalSort.test.js @@ -0,0 +1,53 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import topologicalSort from '../topologicalSort'; + +describe('topologicalSort', () => { + it('should do topological sorting on graph', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + const vertexD = new GraphVertex('D'); + const vertexE = new GraphVertex('E'); + const vertexF = new GraphVertex('F'); + const vertexG = new GraphVertex('G'); + const vertexH = new GraphVertex('H'); + + const edgeAC = new GraphEdge(vertexA, vertexC); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeBD = new GraphEdge(vertexB, vertexD); + const edgeCE = new GraphEdge(vertexC, vertexE); + const edgeDF = new GraphEdge(vertexD, vertexF); + const edgeEF = new GraphEdge(vertexE, vertexF); + const edgeEH = new GraphEdge(vertexE, vertexH); + const edgeFG = new GraphEdge(vertexF, vertexG); + + const graph = new Graph(true); + + graph + .addEdge(edgeAC) + .addEdge(edgeBC) + .addEdge(edgeBD) + .addEdge(edgeCE) + .addEdge(edgeDF) + .addEdge(edgeEF) + .addEdge(edgeEH) + .addEdge(edgeFG); + + const sortedVertices = topologicalSort(graph); + + expect(sortedVertices).toBeDefined(); + expect(sortedVertices.length).toBe(graph.getAllVertices().length); + expect(sortedVertices).toEqual([ + vertexB, + vertexD, + vertexA, + vertexC, + vertexE, + vertexH, + vertexF, + vertexG, + ]); + }); +}); diff --git a/src/algorithms/graph/topological-sorting/topologicalSort.js b/src/algorithms/graph/topological-sorting/topologicalSort.js new file mode 100644 index 00000000..cd7bdd3d --- /dev/null +++ b/src/algorithms/graph/topological-sorting/topologicalSort.js @@ -0,0 +1,47 @@ +import Stack from '../../../data-structures/stack/Stack'; +import depthFirstSearch from '../depth-first-search/depthFirstSearch'; + +/** + * @param {Graph} graph + */ +export default function topologicalSort(graph) { + // Create a set of all vertices we want to visit. + const unvisitedSet = {}; + graph.getAllVertices().forEach((vertex) => { + unvisitedSet[vertex.getKey()] = vertex; + }); + + // Create a set for all vertices that we've already visited. + const visitedSet = {}; + + // Create a stack of already ordered vertices. + const sortedStack = new Stack(); + + const dfsCallbacks = { + enterVertex: ({ currentVertex }) => { + // Add vertex to visited set in case if all its children has been explored. + visitedSet[currentVertex.getKey()] = currentVertex; + + // Remove this vertex from unvisited set. + delete unvisitedSet[currentVertex.getKey()]; + }, + leaveVertex: ({ currentVertex }) => { + // If the vertex has been totally explored then we may push it to stack. + sortedStack.push(currentVertex); + }, + allowTraversal: ({ nextVertex }) => { + return !visitedSet[nextVertex.getKey()]; + }, + }; + + // Let's go and do DFS for all unvisited nodes. + while (Object.keys(unvisitedSet).length) { + const currentVertexKey = Object.keys(unvisitedSet)[0]; + const currentVertex = unvisitedSet[currentVertexKey]; + + // Do DFS for current node. + depthFirstSearch(graph, currentVertex, dfsCallbacks); + } + + return sortedStack.toArray(); +} diff --git a/src/data-structures/linked-list/LinkedList.js b/src/data-structures/linked-list/LinkedList.js index 66dbdb90..13537525 100644 --- a/src/data-structures/linked-list/LinkedList.js +++ b/src/data-structures/linked-list/LinkedList.js @@ -9,6 +9,10 @@ export default class LinkedList { this.tail = null; } + /** + * @param {*} value + * @return {LinkedList} + */ prepend(value) { // Make new node to be a head. this.head = new LinkedListNode(value, this.head); @@ -16,6 +20,10 @@ export default class LinkedList { return this; } + /** + * @param {*} value + * @return {LinkedList} + */ append(value) { const newNode = new LinkedListNode(value); @@ -34,6 +42,10 @@ export default class LinkedList { return this; } + /** + * @param {*} value + * @return {LinkedListNode} + */ delete(value) { if (!this.head) { return null; @@ -67,6 +79,12 @@ export default class LinkedList { return deletedNode; } + /** + * @param {Object} findParams + * @param {*} findParams.value + * @param {function} [findParams.callback] + * @return {LinkedListNode} + */ find({ value = undefined, callback = undefined }) { if (!this.head) { return null; @@ -91,6 +109,9 @@ export default class LinkedList { return null; } + /** + * @return {LinkedListNode} + */ deleteTail() { if (this.head === this.tail) { const deletedTail = this.tail; @@ -116,6 +137,9 @@ export default class LinkedList { return deletedTail; } + /** + * @return {LinkedListNode} + */ deleteHead() { if (!this.head) { return null; @@ -133,6 +157,9 @@ export default class LinkedList { return deletedHead; } + /** + * @return {LinkedListNode[]} + */ toArray() { const nodes = []; @@ -145,6 +172,10 @@ export default class LinkedList { return nodes; } + /** + * @param {function} [callback] + * @return {string} + */ toString(callback) { return this.toArray().map(node => node.toString(callback)).toString(); } diff --git a/src/data-structures/stack/Stack.js b/src/data-structures/stack/Stack.js index 665bec2e..a7108300 100644 --- a/src/data-structures/stack/Stack.js +++ b/src/data-structures/stack/Stack.js @@ -5,10 +5,16 @@ export default class Stack { this.linkedList = new LinkedList(); } + /** + * @return {boolean} + */ isEmpty() { return !this.linkedList.tail; } + /** + * @return {LinkedListNode} + */ peek() { if (!this.linkedList.tail) { return null; @@ -17,15 +23,35 @@ export default class Stack { return this.linkedList.tail.value; } + /** + * @param {*} value + */ push(value) { this.linkedList.append(value); } + /** + * @return {LinkedListNode} + */ pop() { const removedTail = this.linkedList.deleteTail(); return removedTail ? removedTail.value : null; } + /** + * @return {*[]} + */ + toArray() { + return this.linkedList + .toArray() + .map(linkedListNode => linkedListNode.value) + .reverse(); + } + + /** + * @param {function} [callback] + * @return {string} + */ toString(callback) { return this.linkedList.toString(callback); } diff --git a/src/data-structures/stack/__test__/Stack.test.js b/src/data-structures/stack/__test__/Stack.test.js index 291bc430..26d121c8 100644 --- a/src/data-structures/stack/__test__/Stack.test.js +++ b/src/data-structures/stack/__test__/Stack.test.js @@ -62,4 +62,16 @@ describe('Stack', () => { expect(stack.pop().value).toBe('test2'); expect(stack.pop().value).toBe('test1'); }); + + it('should be possible to convert stack to array', () => { + const stack = new Stack(); + + expect(stack.peek()).toBeNull(); + + stack.push(1); + stack.push(2); + stack.push(3); + + expect(stack.toArray()).toEqual([3, 2, 1]); + }); });