Add BFS and DFS for tree.

This commit is contained in:
Oleksii Trekhleb 2018-04-25 17:35:44 +03:00
parent b175aabf8d
commit 85585769ea
8 changed files with 346 additions and 6 deletions

View File

@ -55,9 +55,12 @@
* [Merge Sort](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sorting/merge-sort) * [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) * [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) * [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** * **Graph**
* [Depth-First Search (DFS)](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/depth-first-search) * [Depth-First Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/depth-first-search) (DFS)
* [Breadth-First Search (BFS)](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/breadth-first-search) * [Breadth-First Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/breadth-first-search) (BFS)
* Detect Cycle * Detect Cycle
* Topological Sorting * Topological Sorting
* Dijkstra Algorithm to Find Shortest Path * Dijkstra Algorithm to Find Shortest Path
@ -65,9 +68,6 @@
* Bellman Ford * Bellman Ford
* Strongly Connected Component algorithm * Strongly Connected Component algorithm
* Shortest Path Faster Algorithm (SPFA) * Shortest Path Faster Algorithm (SPFA)
* **Tree**
* Depth-First Search (DFS)
* Breadth-First Search (BFS)
* **Minimum Spanning Tree** * **Minimum Spanning Tree**
* Prims algorithm * Prims algorithm
* Kruskals algorithm * Kruskals algorithm

View File

@ -39,7 +39,7 @@ describe('depthFirstSearch', () => {
const enterVertexCallback = jest.fn(); const enterVertexCallback = jest.fn();
const leaveVertexCallback = jest.fn(); const leaveVertexCallback = jest.fn();
// Traverse graphs without callbacks first. // Traverse graphs without callbacks first to check default ones.
depthFirstSearch(graph, vertexA); depthFirstSearch(graph, vertexA);
// Traverse graph with enterVertex and leaveVertex callbacks. // Traverse graph with enterVertex and leaveVertex callbacks.

View File

@ -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)

View File

@ -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');
});
});

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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');
});
});

View File

@ -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));
}