diff --git a/README.md b/README.md index c7e6501a..0076de42 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ * [Power Set](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/math/power-set) * String * [String Permutations](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/string/permutations) +* Graph + * [Depth-First Search (DFS)](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/depth-first-search) ## Useful Links diff --git a/src/algorithms/graph/depth-first-search/README.md b/src/algorithms/graph/depth-first-search/README.md new file mode 100644 index 00000000..84eb9c7b --- /dev/null +++ b/src/algorithms/graph/depth-first-search/README.md @@ -0,0 +1,7 @@ +# Depth-First Search (DFS) + +Depth-first search (DFS) is an algorithm for traversing or +searching tree or graph data structures. One starts at +the root (selecting some arbitrary node as the root in +the case of a graph) and explores as far as possible +along each branch before backtracking. diff --git a/src/algorithms/graph/depth-first-search/__test__/depthFirstSearch.test.js b/src/algorithms/graph/depth-first-search/__test__/depthFirstSearch.test.js new file mode 100644 index 00000000..1b556186 --- /dev/null +++ b/src/algorithms/graph/depth-first-search/__test__/depthFirstSearch.test.js @@ -0,0 +1,107 @@ +import Graph from '../../../../data-structures/graph/Graph'; +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import depthFirstSearch from '../depthFirstSearch'; + +describe('depthFirstSearch', () => { + it('should perform DFS operation on graph', () => { + const graph = new Graph(true); + + 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 edgeAB = new GraphEdge(vertexA, vertexB); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeCG = new GraphEdge(vertexC, vertexG); + const edgeAD = new GraphEdge(vertexA, vertexD); + const edgeAE = new GraphEdge(vertexA, vertexE); + const edgeEF = new GraphEdge(vertexE, vertexF); + const edgeFD = new GraphEdge(vertexF, vertexD); + const edgeDG = new GraphEdge(vertexD, vertexG); + + graph + .addEdge(edgeAB) + .addEdge(edgeBC) + .addEdge(edgeCG) + .addEdge(edgeAD) + .addEdge(edgeAE) + .addEdge(edgeEF) + .addEdge(edgeFD) + .addEdge(edgeDG); + + expect(graph.toString()).toBe('A,B,C,G,D,E,F'); + + const enterVertexCallback = jest.fn(); + const leaveVertexCallback = jest.fn(); + + // Traverse graphs without callbacks first. + depthFirstSearch(graph, vertexA); + + // Traverse graph with enterVertex and leaveVertex callbacks. + depthFirstSearch(graph, vertexA, { + enterVertex: enterVertexCallback, + leaveVertex: leaveVertexCallback, + }); + + expect(enterVertexCallback).toHaveBeenCalledTimes(7); + expect(leaveVertexCallback).toHaveBeenCalledTimes(7); + + expect(enterVertexCallback.mock.calls.toString()).toBe('A,B,C,G,D,E,F'); + expect(leaveVertexCallback.mock.calls.toString()).toBe('G,C,B,D,F,E,A'); + }); + + it('allow users to redefine vertex visiting logic', () => { + const graph = new Graph(true); + + 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 edgeAB = new GraphEdge(vertexA, vertexB); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeCG = new GraphEdge(vertexC, vertexG); + const edgeAD = new GraphEdge(vertexA, vertexD); + const edgeAE = new GraphEdge(vertexA, vertexE); + const edgeEF = new GraphEdge(vertexE, vertexF); + const edgeFD = new GraphEdge(vertexF, vertexD); + const edgeDG = new GraphEdge(vertexD, vertexG); + + graph + .addEdge(edgeAB) + .addEdge(edgeBC) + .addEdge(edgeCG) + .addEdge(edgeAD) + .addEdge(edgeAE) + .addEdge(edgeEF) + .addEdge(edgeFD) + .addEdge(edgeDG); + + expect(graph.toString()).toBe('A,B,C,G,D,E,F'); + + const enterVertexCallback = jest.fn(); + const leaveVertexCallback = jest.fn(); + + depthFirstSearch(graph, vertexA, { + enterVertex: enterVertexCallback, + leaveVertex: leaveVertexCallback, + allowTraversal: (vertex, neighbor) => { + return !(vertex === vertexA && neighbor === vertexB); + }, + }); + + expect(enterVertexCallback).toHaveBeenCalledTimes(7); + expect(leaveVertexCallback).toHaveBeenCalledTimes(7); + + expect(enterVertexCallback.mock.calls.toString()).toBe('A,D,G,E,F,D,G'); + expect(leaveVertexCallback.mock.calls.toString()).toBe('G,D,G,D,F,E,A'); + }); +}); diff --git a/src/algorithms/graph/depth-first-search/depthFirstSearch.js b/src/algorithms/graph/depth-first-search/depthFirstSearch.js new file mode 100644 index 00000000..85c2f98c --- /dev/null +++ b/src/algorithms/graph/depth-first-search/depthFirstSearch.js @@ -0,0 +1,63 @@ +/** + * @typedef {Object} Callbacks + * @property {function(vertex: GraphVertex, neighbor: GraphVertex): boolean} allowTraversal - + * Determines whether DFS should traverse from the vertex to its neighbor + * (along the edge). By default prohibits visiting the same vertex again. + * @property {function(vertex: GraphVertex)} enterVertex - Called when DFS enters the vertex. + * @property {function(vertex: GraphVertex)} leaveVertex - Called when DFS leaves the vertex. + */ + +/** + * @param {Callbacks} [callbacks] + * @returns {Callbacks} + */ +function initCallbacks(callbacks = {}) { + const initiatedCallback = callbacks; + + const stubCallback = () => {}; + + const allowTraversalCallback = ( + () => { + const seen = {}; + return (vertex, neighbor) => { + if (!seen[neighbor.getKey()]) { + seen[neighbor.getKey()] = true; + return true; + } + return false; + }; + } + )(); + + initiatedCallback.allowTraversal = callbacks.allowTraversal || allowTraversalCallback; + initiatedCallback.enterVertex = callbacks.enterVertex || stubCallback; + initiatedCallback.leaveVertex = callbacks.leaveVertex || stubCallback; + + return initiatedCallback; +} + +/** + * @param {Graph} graph + * @param {GraphVertex} vertex + * @param {Callbacks} callbacks + */ +function depthFirstSearchRecursive(graph, vertex, callbacks) { + callbacks.enterVertex(vertex); + + graph.getNeighbors(vertex).forEach((neighbor) => { + if (callbacks.allowTraversal(vertex, neighbor)) { + depthFirstSearchRecursive(graph, neighbor, callbacks); + } + }); + + callbacks.leaveVertex(vertex); +} + +/** + * @param {Graph} graph + * @param {GraphVertex} startVertex + * @param {Callbacks} [callbacks] + */ +export default function depthFirstSearch(graph, startVertex, callbacks) { + depthFirstSearchRecursive(graph, startVertex, initCallbacks(callbacks)); +}