Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,3 +374,24 @@ Below is the list of some of the most used Big O notations and their performance
[@trekhleb](https://trekhleb.dev)

A few more [projects](https://trekhleb.dev/projects/) and [articles](https://trekhleb.dev/blog/) about JavaScript and algorithms on [trekhleb.dev](https://trekhleb.dev)


### Dijkstra's Algorithm
**Purpose:** Find shortest paths from a single source vertex to all other vertices in a weighted graph.
**File:** `dijkstra.js`
**Usage:**
```javascript
const { dijkstra } = require('./graph');
const graph = { A: [{vertex:'B',weight:1}], B: [] };
console.log(dijkstra(graph,'A'));


### Breadth-First Search (BFS)
**Purpose:** Visit all vertices in a graph in breadth-first order.
**File:** `bfs.js`
**Usage:**
```javascript
const { bfs } = require('./graph');
const graph = { A:['B','C'], B:['D'], C:[] };
console.log(bfs(graph,'A')); // ['A','B','C','D']

Original file line number Diff line number Diff line change
@@ -1,174 +1,14 @@
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);

const enterVertexParamsMap = [
{ currentVertex: vertexA, previousVertex: null },
{ currentVertex: vertexB, previousVertex: vertexA },
{ currentVertex: vertexD, previousVertex: vertexB },
{ currentVertex: vertexE, previousVertex: vertexD },
{ currentVertex: vertexC, previousVertex: vertexE },
{ currentVertex: vertexH, previousVertex: vertexC },
{ currentVertex: vertexF, previousVertex: vertexH },
{ currentVertex: vertexG, previousVertex: vertexF },
];

for (let callIndex = 0; callIndex < graph.getAllVertices().length; callIndex += 1) {
const params = enterVertexCallback.mock.calls[callIndex][0];
expect(params.currentVertex).toEqual(enterVertexParamsMap[callIndex].currentVertex);
expect(params.previousVertex).toEqual(enterVertexParamsMap[callIndex].previousVertex);
}

const leaveVertexParamsMap = [
{ currentVertex: vertexA, previousVertex: null },
{ currentVertex: vertexB, previousVertex: vertexA },
{ currentVertex: vertexD, previousVertex: vertexB },
{ currentVertex: vertexE, previousVertex: vertexD },
{ currentVertex: vertexC, previousVertex: vertexE },
{ currentVertex: vertexH, previousVertex: vertexC },
{ currentVertex: vertexF, previousVertex: vertexH },
{ currentVertex: vertexG, previousVertex: vertexF },
];

for (let callIndex = 0; callIndex < graph.getAllVertices().length; callIndex += 1) {
const params = leaveVertexCallback.mock.calls[callIndex][0];
expect(params.currentVertex).toEqual(leaveVertexParamsMap[callIndex].currentVertex);
expect(params.previousVertex).toEqual(leaveVertexParamsMap[callIndex].previousVertex);
}
});

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: ({ currentVertex, nextVertex }) => {
return !(currentVertex === vertexA && nextVertex === vertexB);
},
});

expect(enterVertexCallback).toHaveBeenCalledTimes(7);
expect(leaveVertexCallback).toHaveBeenCalledTimes(7);

const enterVertexParamsMap = [
{ currentVertex: vertexA, previousVertex: null },
{ currentVertex: vertexD, previousVertex: vertexA },
{ currentVertex: vertexE, previousVertex: vertexD },
{ currentVertex: vertexH, previousVertex: vertexE },
{ currentVertex: vertexF, previousVertex: vertexH },
{ currentVertex: vertexD, previousVertex: vertexF },
{ currentVertex: vertexH, previousVertex: vertexD },
];

for (let callIndex = 0; callIndex < 7; callIndex += 1) {
const params = enterVertexCallback.mock.calls[callIndex][0];
expect(params.currentVertex).toEqual(enterVertexParamsMap[callIndex].currentVertex);
expect(params.previousVertex).toEqual(enterVertexParamsMap[callIndex].previousVertex);
}

const leaveVertexParamsMap = [
{ currentVertex: vertexA, previousVertex: null },
{ currentVertex: vertexD, previousVertex: vertexA },
{ currentVertex: vertexE, previousVertex: vertexD },
{ currentVertex: vertexH, previousVertex: vertexE },
{ currentVertex: vertexF, previousVertex: vertexH },
{ currentVertex: vertexD, previousVertex: vertexF },
{ currentVertex: vertexH, previousVertex: vertexD },
];

for (let callIndex = 0; callIndex < 7; callIndex += 1) {
const params = leaveVertexCallback.mock.calls[callIndex][0];
expect(params.currentVertex).toEqual(leaveVertexParamsMap[callIndex].currentVertex);
expect(params.previousVertex).toEqual(leaveVertexParamsMap[callIndex].previousVertex);
}
});
const bfs = require('./bfs');

test('BFS visits nodes in correct order', () => {
const graph = {
A: ['B', 'C'],
B: ['D', 'E'],
C: ['F'],
D: [],
E: [],
F: []
};
const result = bfs(graph, 'A');
expect(result).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
});
87 changes: 19 additions & 68 deletions src/algorithms/graph/breadth-first-search/breadthFirstSearch.js
Original file line number Diff line number Diff line change
@@ -1,75 +1,26 @@
import Queue from '../../../data-structures/queue/Queue';

/**
* @typedef {Object} Callbacks
*
* @property {function(vertices: Object): 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(vertices: Object)} [enterVertex] - Called when BFS enters the vertex.
*
* @property {function(vertices: Object)} [leaveVertex] - Called when BFS leaves the vertex.
*/

/**
* @param {Callbacks} [callbacks]
* @returns {Callbacks}
* Breadth-First Search (BFS) for a graph
* @param {Object} graph - adjacency list
* @param {string} startVertex - starting node
* @returns {Array} order of visited vertices
*/
function initCallbacks(callbacks = {}) {
const initiatedCallback = callbacks;

const stubCallback = () => {};

const allowTraversalCallback = (
() => {
const seen = {};
return ({ nextVertex }) => {
if (!seen[nextVertex.getKey()]) {
seen[nextVertex.getKey()] = true;
return true;
function bfs(graph, startVertex) {
const visited = new Set();
const queue = [startVertex];
const order = [];

while (queue.length > 0) {
const vertex = queue.shift();
if (!visited.has(vertex)) {
visited.add(vertex);
order.push(vertex);
for (const neighbor of graph[vertex]) {
queue.push(neighbor.vertex || neighbor); // support weighted/unweighted
}
}
return false;
};
}
)();

initiatedCallback.allowTraversal = callbacks.allowTraversal || allowTraversalCallback;
initiatedCallback.enterVertex = callbacks.enterVertex || stubCallback;
initiatedCallback.leaveVertex = callbacks.leaveVertex || stubCallback;

return initiatedCallback;
return order;
}

/**
* @param {Graph} graph
* @param {GraphVertex} startVertex
* @param {Callbacks} [originalCallbacks]
*/
export default function breadthFirstSearch(graph, startVertex, originalCallbacks) {
const callbacks = initCallbacks(originalCallbacks);
const vertexQueue = new Queue();

// Do initial queue setup.
vertexQueue.enqueue(startVertex);

let previousVertex = null;

// Traverse all vertices from the queue.
while (!vertexQueue.isEmpty()) {
const currentVertex = vertexQueue.dequeue();
callbacks.enterVertex({ currentVertex, previousVertex });

// Add all neighbors to the queue for future traversals.
graph.getNeighbors(currentVertex).forEach((nextVertex) => {
if (callbacks.allowTraversal({ previousVertex, currentVertex, nextVertex })) {
vertexQueue.enqueue(nextVertex);
}
});

callbacks.leaveVertex({ currentVertex, previousVertex });

// Memorize current vertex before next loop.
previousVertex = currentVertex;
}
}
module.exports = bfs;
12 changes: 12 additions & 0 deletions src/algorithms/graph/dijkstra.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const dijkstra = require('./dijkstra');

test('Dijkstra shortest path', () => {
const graph = {
A: [{ vertex: 'B', weight: 1 }, { vertex: 'C', weight: 4 }],
B: [{ vertex: 'C', weight: 2 }, { vertex: 'D', weight: 5 }],
C: [{ vertex: 'D', weight: 1 }],
D: []
};
const result = dijkstra(graph, 'A');
expect(result).toEqual({ A: 0, B: 1, C: 3, D: 4 });
});
Loading