diff --git a/README.md b/README.md index 0076de42..a55e2d99 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ * [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) + * [Breadth-First Search (BFS)](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/breadth-first-search) ## Useful Links diff --git a/src/algorithms/graph/breadth-first-search/README.md b/src/algorithms/graph/breadth-first-search/README.md new file mode 100644 index 00000000..3fdeda21 --- /dev/null +++ b/src/algorithms/graph/breadth-first-search/README.md @@ -0,0 +1,7 @@ +# Breadth-First Search (BFS) + +Breadth-first search (BFS) is an algorithm for traversing +or searching tree or graph data structures. It starts at +the tree root (or some arbitrary node of a graph, sometimes +referred to as a 'search key') and explores the neighbor +nodes first, before moving to the next level neighbors. diff --git a/src/algorithms/graph/breadth-first-search/__test__/breadthFirstSearch.test.js b/src/algorithms/graph/breadth-first-search/__test__/breadthFirstSearch.test.js new file mode 100644 index 00000000..a9157265 --- /dev/null +++ b/src/algorithms/graph/breadth-first-search/__test__/breadthFirstSearch.test.js @@ -0,0 +1,114 @@ +import Graph from '../../../../data-structures/graph/Graph'; +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import breadthFirstSearch from '../breadthFirstSearch'; + +describe('breadthFirstSearch', () => { + it('should perform BFS 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 vertexH = new GraphVertex('H'); + + 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 edgeDH = new GraphEdge(vertexD, vertexH); + const edgeGH = new GraphEdge(vertexG, vertexH); + + graph + .addEdge(edgeAB) + .addEdge(edgeBC) + .addEdge(edgeCG) + .addEdge(edgeAD) + .addEdge(edgeAE) + .addEdge(edgeEF) + .addEdge(edgeFD) + .addEdge(edgeDH) + .addEdge(edgeGH); + + expect(graph.toString()).toBe('A,B,C,G,D,E,F,H'); + + const enterVertexCallback = jest.fn(); + const leaveVertexCallback = jest.fn(); + + // Traverse graphs without callbacks first. + breadthFirstSearch(graph, vertexA); + + // Traverse graph with enterVertex and leaveVertex callbacks. + breadthFirstSearch(graph, vertexA, { + enterVertex: enterVertexCallback, + leaveVertex: leaveVertexCallback, + }); + + expect(enterVertexCallback).toHaveBeenCalledTimes(8); + expect(leaveVertexCallback).toHaveBeenCalledTimes(8); + + expect(enterVertexCallback.mock.calls.toString()).toBe('A,B,D,E,C,H,F,G'); + expect(leaveVertexCallback.mock.calls.toString()).toBe('A,B,D,E,C,H,F,G'); + }); + + it('should allow to create custom 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 vertexH = new GraphVertex('H'); + + 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 edgeDH = new GraphEdge(vertexD, vertexH); + const edgeGH = new GraphEdge(vertexG, vertexH); + + graph + .addEdge(edgeAB) + .addEdge(edgeBC) + .addEdge(edgeCG) + .addEdge(edgeAD) + .addEdge(edgeAE) + .addEdge(edgeEF) + .addEdge(edgeFD) + .addEdge(edgeDH) + .addEdge(edgeGH); + + expect(graph.toString()).toBe('A,B,C,G,D,E,F,H'); + + const enterVertexCallback = jest.fn(); + const leaveVertexCallback = jest.fn(); + + // Traverse graph with enterVertex and leaveVertex callbacks. + breadthFirstSearch(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,E,H,F,D,H'); + expect(leaveVertexCallback.mock.calls.toString()).toBe('A,D,E,H,F,D,H'); + }); +}); diff --git a/src/algorithms/graph/breadth-first-search/breadthFirstSearch.js b/src/algorithms/graph/breadth-first-search/breadthFirstSearch.js new file mode 100644 index 00000000..6ef59c0b --- /dev/null +++ b/src/algorithms/graph/breadth-first-search/breadthFirstSearch.js @@ -0,0 +1,67 @@ +import Queue from '../../../data-structures/queue/Queue'; + +/** + * @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} startVertex + * @param {Callbacks} [rawCallbacks] + */ +export default function breadthFirstSearch(graph, startVertex, rawCallbacks) { + const callbacks = initCallbacks(rawCallbacks); + const vertexQueue = new Queue(); + + // Do initial queue setup. + vertexQueue.enqueue(startVertex); + + // Traverse all vertices from the queue. + while (!vertexQueue.isEmpty()) { + const currentVertex = vertexQueue.dequeue(); + callbacks.enterVertex(currentVertex); + + // Add all neighbors to the queue for future traversals. + graph.getNeighbors(currentVertex).forEach((neighbor) => { + if (callbacks.allowTraversal(currentVertex, neighbor)) { + vertexQueue.enqueue(neighbor); + } + }); + + callbacks.leaveVertex(currentVertex); + } +} diff --git a/src/algorithms/graph/depth-first-search/README.md b/src/algorithms/graph/depth-first-search/README.md index 8ec1a409..84eb9c7b 100644 --- a/src/algorithms/graph/depth-first-search/README.md +++ b/src/algorithms/graph/depth-first-search/README.md @@ -5,12 +5,3 @@ 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. - -## Complexity - -* Time: O(|V| + |E|) -* Space: O(|V|) - -## References - -[Wikipedia](https://en.wikipedia.org/wiki/Depth-first_search)