diff --git a/README.md b/README.md index 4aee77fa..6336b7da 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,12 @@ * [Merge Sort](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sorting/merge-sort) * [Quicksort](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sorting/quick-sort) * [Shellsort](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sorting/shell-sort) +* **Tree** + * [Depth-First Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/tree/depth-first-search) (DFS) + * [Breadth-First Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/tree/breadth-first-search) (BFS) * **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) + * [Depth-First Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/depth-first-search) (DFS) + * [Breadth-First Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/breadth-first-search) (BFS) * Detect Cycle * Topological Sorting * Dijkstra Algorithm to Find Shortest Path @@ -65,9 +68,6 @@ * Bellman Ford * Strongly Connected Component algorithm * Shortest Path Faster Algorithm (SPFA) -* **Tree** - * Depth-First Search (DFS) - * Breadth-First Search (BFS) * **Minimum Spanning Tree** * Prim’s algorithm * Kruskal’s algorithm diff --git a/src/algorithms/graph/depth-first-search/__test__/depthFirstSearch.test.js b/src/algorithms/graph/depth-first-search/__test__/depthFirstSearch.test.js index 1b556186..52806dc8 100644 --- a/src/algorithms/graph/depth-first-search/__test__/depthFirstSearch.test.js +++ b/src/algorithms/graph/depth-first-search/__test__/depthFirstSearch.test.js @@ -39,7 +39,7 @@ describe('depthFirstSearch', () => { const enterVertexCallback = jest.fn(); const leaveVertexCallback = jest.fn(); - // Traverse graphs without callbacks first. + // Traverse graphs without callbacks first to check default ones. depthFirstSearch(graph, vertexA); // Traverse graph with enterVertex and leaveVertex callbacks. diff --git a/src/algorithms/tree/breadth-first-search/README.md b/src/algorithms/tree/breadth-first-search/README.md new file mode 100644 index 00000000..d71dc7ec --- /dev/null +++ b/src/algorithms/tree/breadth-first-search/README.md @@ -0,0 +1,13 @@ +# 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. + +![Algorithm Visualization](https://upload.wikimedia.org/wikipedia/commons/5/5d/Breadth-First-Search-Algorithm.gif) + +## References + +[Wikipedia](https://en.wikipedia.org/wiki/Breadth-first_search) diff --git a/src/algorithms/tree/breadth-first-search/__test__/breadthFirstSearch.test.js b/src/algorithms/tree/breadth-first-search/__test__/breadthFirstSearch.test.js new file mode 100644 index 00000000..4a77f0f3 --- /dev/null +++ b/src/algorithms/tree/breadth-first-search/__test__/breadthFirstSearch.test.js @@ -0,0 +1,102 @@ +import BinaryTreeNode from '../../../../data-structures/tree/BinaryTreeNode'; +import breadthFirstSearch from '../breadthFirstSearch'; + +describe('breadthFirstSearch', () => { + it('should perform DFS operation on tree', () => { + const nodeA = new BinaryTreeNode('A'); + const nodeB = new BinaryTreeNode('B'); + const nodeC = new BinaryTreeNode('C'); + const nodeD = new BinaryTreeNode('D'); + const nodeE = new BinaryTreeNode('E'); + const nodeF = new BinaryTreeNode('F'); + const nodeG = new BinaryTreeNode('G'); + + nodeA.setLeft(nodeB).setRight(nodeC); + nodeB.setLeft(nodeD).setRight(nodeE); + nodeC.setLeft(nodeF).setRight(nodeG); + + // In-order traversing. + expect(nodeA.toString()).toBe('D,B,E,A,F,C,G'); + + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + + // Traverse tree without callbacks first to check default ones. + breadthFirstSearch(nodeA); + + // Traverse tree with callbacks. + breadthFirstSearch(nodeA, { + enterNode: enterNodeCallback, + leaveNode: leaveNodeCallback, + }); + + expect(enterNodeCallback).toHaveBeenCalledTimes(7); + expect(leaveNodeCallback).toHaveBeenCalledTimes(7); + + // Check node entering. + expect(enterNodeCallback.mock.calls[0][0].value).toEqual('A'); + expect(enterNodeCallback.mock.calls[1][0].value).toEqual('B'); + expect(enterNodeCallback.mock.calls[2][0].value).toEqual('C'); + expect(enterNodeCallback.mock.calls[3][0].value).toEqual('D'); + expect(enterNodeCallback.mock.calls[4][0].value).toEqual('E'); + expect(enterNodeCallback.mock.calls[5][0].value).toEqual('F'); + expect(enterNodeCallback.mock.calls[6][0].value).toEqual('G'); + + // Check node leaving. + expect(leaveNodeCallback.mock.calls[0][0].value).toEqual('A'); + expect(leaveNodeCallback.mock.calls[1][0].value).toEqual('B'); + expect(leaveNodeCallback.mock.calls[2][0].value).toEqual('C'); + expect(leaveNodeCallback.mock.calls[3][0].value).toEqual('D'); + expect(leaveNodeCallback.mock.calls[4][0].value).toEqual('E'); + expect(leaveNodeCallback.mock.calls[5][0].value).toEqual('F'); + expect(leaveNodeCallback.mock.calls[6][0].value).toEqual('G'); + }); + + it('allow users to redefine node visiting logic', () => { + const nodeA = new BinaryTreeNode('A'); + const nodeB = new BinaryTreeNode('B'); + const nodeC = new BinaryTreeNode('C'); + const nodeD = new BinaryTreeNode('D'); + const nodeE = new BinaryTreeNode('E'); + const nodeF = new BinaryTreeNode('F'); + const nodeG = new BinaryTreeNode('G'); + + nodeA.setLeft(nodeB).setRight(nodeC); + nodeB.setLeft(nodeD).setRight(nodeE); + nodeC.setLeft(nodeF).setRight(nodeG); + + // In-order traversing. + expect(nodeA.toString()).toBe('D,B,E,A,F,C,G'); + + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + + // Traverse tree without callbacks first to check default ones. + breadthFirstSearch(nodeA); + + // Traverse tree with callbacks. + breadthFirstSearch(nodeA, { + allowTraversal: (node, child) => { + // Forbid traversing left half of the tree. + return child.value !== 'B'; + }, + enterNode: enterNodeCallback, + leaveNode: leaveNodeCallback, + }); + + expect(enterNodeCallback).toHaveBeenCalledTimes(4); + expect(leaveNodeCallback).toHaveBeenCalledTimes(4); + + // Check node entering. + expect(enterNodeCallback.mock.calls[0][0].value).toEqual('A'); + expect(enterNodeCallback.mock.calls[1][0].value).toEqual('C'); + expect(enterNodeCallback.mock.calls[2][0].value).toEqual('F'); + expect(enterNodeCallback.mock.calls[3][0].value).toEqual('G'); + + // Check node leaving. + expect(leaveNodeCallback.mock.calls[0][0].value).toEqual('A'); + expect(leaveNodeCallback.mock.calls[1][0].value).toEqual('C'); + expect(leaveNodeCallback.mock.calls[2][0].value).toEqual('F'); + expect(leaveNodeCallback.mock.calls[3][0].value).toEqual('G'); + }); +}); diff --git a/src/algorithms/tree/breadth-first-search/breadthFirstSearch.js b/src/algorithms/tree/breadth-first-search/breadthFirstSearch.js new file mode 100644 index 00000000..503ec34f --- /dev/null +++ b/src/algorithms/tree/breadth-first-search/breadthFirstSearch.js @@ -0,0 +1,58 @@ +import Queue from '../../../data-structures/queue/Queue'; + +/** + * @typedef {Object} Callbacks + * @property {function(node: BinaryTreeNode, child: BinaryTreeNode): boolean} allowTraversal - + * Determines whether DFS should traverse from the node to its child. + * @property {function(node: BinaryTreeNode)} enterNode - Called when DFS enters the node. + * @property {function(node: BinaryTreeNode)} leaveNode - Called when DFS leaves the node. + */ + +/** + * @param {Callbacks} [callbacks] + * @returns {Callbacks} + */ +function initCallbacks(callbacks = {}) { + const initiatedCallback = callbacks; + + const stubCallback = () => {}; + const defaultAllowTraversal = () => true; + + initiatedCallback.allowTraversal = callbacks.allowTraversal || defaultAllowTraversal; + initiatedCallback.enterNode = callbacks.enterNode || stubCallback; + initiatedCallback.leaveNode = callbacks.leaveNode || stubCallback; + + return initiatedCallback; +} + +/** + * @param {BinaryTreeNode} rootNode + * @param {Callbacks} [originalCallbacks] + */ +export default function breadthFirstSearch(rootNode, originalCallbacks) { + const callbacks = initCallbacks(originalCallbacks); + const nodeQueue = new Queue(); + + // Do initial queue setup. + nodeQueue.enqueue(rootNode); + + while (!nodeQueue.isEmpty()) { + const currentNode = nodeQueue.dequeue(); + + callbacks.enterNode(currentNode); + + // Add all children to the queue for future traversals. + + // Traverse left branch. + if (currentNode.left && callbacks.allowTraversal(currentNode, currentNode.left)) { + nodeQueue.enqueue(currentNode.left); + } + + // Traverse right branch. + if (currentNode.right && callbacks.allowTraversal(currentNode, currentNode.right)) { + nodeQueue.enqueue(currentNode.right); + } + + callbacks.leaveNode(currentNode); + } +} diff --git a/src/algorithms/tree/depth-first-search/README.md b/src/algorithms/tree/depth-first-search/README.md new file mode 100644 index 00000000..be3565d1 --- /dev/null +++ b/src/algorithms/tree/depth-first-search/README.md @@ -0,0 +1,13 @@ +# 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. + +![Algorithm Visualization](https://upload.wikimedia.org/wikipedia/commons/7/7f/Depth-First-Search.gif) + +## References + +[Wikipedia](https://en.wikipedia.org/wiki/Depth-first_search) diff --git a/src/algorithms/tree/depth-first-search/__test__/depthFirstSearch.test.js b/src/algorithms/tree/depth-first-search/__test__/depthFirstSearch.test.js new file mode 100644 index 00000000..732b7d50 --- /dev/null +++ b/src/algorithms/tree/depth-first-search/__test__/depthFirstSearch.test.js @@ -0,0 +1,102 @@ +import BinaryTreeNode from '../../../../data-structures/tree/BinaryTreeNode'; +import depthFirstSearch from '../depthFirstSearch'; + +describe('depthFirstSearch', () => { + it('should perform DFS operation on tree', () => { + const nodeA = new BinaryTreeNode('A'); + const nodeB = new BinaryTreeNode('B'); + const nodeC = new BinaryTreeNode('C'); + const nodeD = new BinaryTreeNode('D'); + const nodeE = new BinaryTreeNode('E'); + const nodeF = new BinaryTreeNode('F'); + const nodeG = new BinaryTreeNode('G'); + + nodeA.setLeft(nodeB).setRight(nodeC); + nodeB.setLeft(nodeD).setRight(nodeE); + nodeC.setLeft(nodeF).setRight(nodeG); + + // In-order traversing. + expect(nodeA.toString()).toBe('D,B,E,A,F,C,G'); + + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + + // Traverse tree without callbacks first to check default ones. + depthFirstSearch(nodeA); + + // Traverse tree with callbacks. + depthFirstSearch(nodeA, { + enterNode: enterNodeCallback, + leaveNode: leaveNodeCallback, + }); + + expect(enterNodeCallback).toHaveBeenCalledTimes(7); + expect(leaveNodeCallback).toHaveBeenCalledTimes(7); + + // Check node entering. + expect(enterNodeCallback.mock.calls[0][0].value).toEqual('A'); + expect(enterNodeCallback.mock.calls[1][0].value).toEqual('B'); + expect(enterNodeCallback.mock.calls[2][0].value).toEqual('D'); + expect(enterNodeCallback.mock.calls[3][0].value).toEqual('E'); + expect(enterNodeCallback.mock.calls[4][0].value).toEqual('C'); + expect(enterNodeCallback.mock.calls[5][0].value).toEqual('F'); + expect(enterNodeCallback.mock.calls[6][0].value).toEqual('G'); + + // Check node leaving. + expect(leaveNodeCallback.mock.calls[0][0].value).toEqual('D'); + expect(leaveNodeCallback.mock.calls[1][0].value).toEqual('E'); + expect(leaveNodeCallback.mock.calls[2][0].value).toEqual('B'); + expect(leaveNodeCallback.mock.calls[3][0].value).toEqual('F'); + expect(leaveNodeCallback.mock.calls[4][0].value).toEqual('G'); + expect(leaveNodeCallback.mock.calls[5][0].value).toEqual('C'); + expect(leaveNodeCallback.mock.calls[6][0].value).toEqual('A'); + }); + + it('allow users to redefine node visiting logic', () => { + const nodeA = new BinaryTreeNode('A'); + const nodeB = new BinaryTreeNode('B'); + const nodeC = new BinaryTreeNode('C'); + const nodeD = new BinaryTreeNode('D'); + const nodeE = new BinaryTreeNode('E'); + const nodeF = new BinaryTreeNode('F'); + const nodeG = new BinaryTreeNode('G'); + + nodeA.setLeft(nodeB).setRight(nodeC); + nodeB.setLeft(nodeD).setRight(nodeE); + nodeC.setLeft(nodeF).setRight(nodeG); + + // In-order traversing. + expect(nodeA.toString()).toBe('D,B,E,A,F,C,G'); + + const enterNodeCallback = jest.fn(); + const leaveNodeCallback = jest.fn(); + + // Traverse tree without callbacks first to check default ones. + depthFirstSearch(nodeA); + + // Traverse tree with callbacks. + depthFirstSearch(nodeA, { + allowTraversal: (node, child) => { + // Forbid traversing left part of the tree. + return child.value !== 'B'; + }, + enterNode: enterNodeCallback, + leaveNode: leaveNodeCallback, + }); + + expect(enterNodeCallback).toHaveBeenCalledTimes(4); + expect(leaveNodeCallback).toHaveBeenCalledTimes(4); + + // Check node entering. + expect(enterNodeCallback.mock.calls[0][0].value).toEqual('A'); + expect(enterNodeCallback.mock.calls[1][0].value).toEqual('C'); + expect(enterNodeCallback.mock.calls[2][0].value).toEqual('F'); + expect(enterNodeCallback.mock.calls[3][0].value).toEqual('G'); + + // Check node leaving. + expect(leaveNodeCallback.mock.calls[0][0].value).toEqual('F'); + expect(leaveNodeCallback.mock.calls[1][0].value).toEqual('G'); + expect(leaveNodeCallback.mock.calls[2][0].value).toEqual('C'); + expect(leaveNodeCallback.mock.calls[3][0].value).toEqual('A'); + }); +}); diff --git a/src/algorithms/tree/depth-first-search/depthFirstSearch.js b/src/algorithms/tree/depth-first-search/depthFirstSearch.js new file mode 100644 index 00000000..188e4a49 --- /dev/null +++ b/src/algorithms/tree/depth-first-search/depthFirstSearch.js @@ -0,0 +1,52 @@ +/** + * @typedef {Object} Callbacks + * @property {function(node: BinaryTreeNode, child: BinaryTreeNode): boolean} allowTraversal - + * Determines whether DFS should traverse from the node to its child. + * @property {function(node: BinaryTreeNode)} enterNode - Called when DFS enters the node. + * @property {function(node: BinaryTreeNode)} leaveNode - Called when DFS leaves the node. + */ + +/** + * @param {Callbacks} [callbacks] + * @returns {Callbacks} + */ +function initCallbacks(callbacks = {}) { + const initiatedCallback = callbacks; + + const stubCallback = () => {}; + const defaultAllowTraversal = () => true; + + initiatedCallback.allowTraversal = callbacks.allowTraversal || defaultAllowTraversal; + initiatedCallback.enterNode = callbacks.enterNode || stubCallback; + initiatedCallback.leaveNode = callbacks.leaveNode || stubCallback; + + return initiatedCallback; +} + +/** + * @param {BinaryTreeNode} node + * @param {Callbacks} callbacks + */ +export function depthFirstSearchRecursive(node, callbacks) { + callbacks.enterNode(node); + + // Traverse left branch. + if (node.left && callbacks.allowTraversal(node, node.left)) { + depthFirstSearchRecursive(node.left, callbacks); + } + + // Traverse right branch. + if (node.right && callbacks.allowTraversal(node, node.right)) { + depthFirstSearchRecursive(node.right, callbacks); + } + + callbacks.leaveNode(node); +} + +/** + * @param {BinaryTreeNode} rootNode + * @param {Callbacks} [callbacks] + */ +export default function depthFirstSearch(rootNode, callbacks) { + depthFirstSearchRecursive(rootNode, initCallbacks(callbacks)); +}