diff --git a/.eslintrc b/.eslintrc index 0d960fe8..b05cfb68 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,7 @@ "rules": { "no-bitwise": "off", "no-lonely-if": "off", - "class-methods-use-this": "off" + "class-methods-use-this": "off", + "arrow-body-style": "off" } } diff --git a/README.md b/README.md index ffbad512..c7e6501a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ 8. [Tree](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/tree) * [Binary Search Tree](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/tree/binary-search-tree) * [AVL Tree](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/tree/avl-tree) +9. [Graph](https://github.com/trekhleb/javascript-algorithms/tree/master/src/data-structures/graph) ## [Algorithms](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms) diff --git a/src/data-structures/graph/Graph.js b/src/data-structures/graph/Graph.js new file mode 100644 index 00000000..18c7a70f --- /dev/null +++ b/src/data-structures/graph/Graph.js @@ -0,0 +1,88 @@ +export default class Graph { + /** + * @param isDirected {boolean} + */ + constructor(isDirected = false) { + this.vertices = {}; + this.isDirected = isDirected; + } + + /** + * @param newVertex {GraphVertex} + * @returns {Graph} + */ + addVertex(newVertex) { + this.vertices[newVertex.getKey()] = newVertex; + + return this; + } + + /** + * @param vertexKey {string} + * @returns GraphVertex + */ + getVertexByKey(vertexKey) { + return this.vertices[vertexKey]; + } + + /** + * @param edge {GraphEdge} + * @returns {Graph} + */ + addEdge(edge) { + // Try to find and end start vertices. + let startVertex = this.getVertexByKey(edge.startVertex.getKey()); + let endVertex = this.getVertexByKey(edge.endVertex.getKey()); + + // Insert start vertex if it wasn't inserted. + if (!startVertex) { + this.addVertex(edge.startVertex); + startVertex = this.getVertexByKey(edge.startVertex.getKey()); + } + + // Insert end vertex if it wasn't inserted. + if (!endVertex) { + this.addVertex(edge.endVertex); + endVertex = this.getVertexByKey(edge.endVertex.getKey()); + } + + // @TODO: Check if edge has been already added. + + // Add edge to the vertices. + if (this.isDirected) { + // If graph IS directed then add the edge only to start vertex. + startVertex.addEdge(edge); + } else { + // If graph ISN'T directed then add the edge to both vertices. + startVertex.addEdge(edge); + endVertex.addEdge(edge); + } + + return this; + } + + /** + * @param startVertex {GraphVertex} + * @param endVertex {GraphVertex} + */ + findEdge(startVertex, endVertex) { + const vertex = this.getVertexByKey(startVertex.getKey()); + return vertex.findEdge(endVertex); + } + + /** + * @param vertexKey {string} + * @returns {GraphVertex} + */ + findVertexByKey(vertexKey) { + if (this.vertices[vertexKey]) { + return this.vertices[vertexKey]; + } + + return null; + } + + toString() { + return Object.keys(this.vertices).toString(); + } +} diff --git a/src/data-structures/graph/GraphEdge.js b/src/data-structures/graph/GraphEdge.js new file mode 100644 index 00000000..386857ff --- /dev/null +++ b/src/data-structures/graph/GraphEdge.js @@ -0,0 +1,12 @@ +export default class GraphEdge { + /** + * @param startVertex {GraphVertex} + * @param endVertex {GraphVertex} + * @param weight {number} + */ + constructor(startVertex, endVertex, weight = 1) { + this.startVertex = startVertex; + this.endVertex = endVertex; + this.weight = weight; + } +} diff --git a/src/data-structures/graph/GraphVertex.js b/src/data-structures/graph/GraphVertex.js new file mode 100644 index 00000000..a598af16 --- /dev/null +++ b/src/data-structures/graph/GraphVertex.js @@ -0,0 +1,85 @@ +import LinkedList from '../linked-list/LinkedList'; + +export default class GraphVertex { + constructor(value) { + if (value === undefined) { + throw new Error('Graph vertex must have a value'); + } + + // Normally you would store string value like vertex name. + // But generally it may be any object as well + this.value = value; + this.edges = new LinkedList(); + } + + /** + * @param edge {GraphEdge} + * @returns {GraphVertex} + */ + addEdge(edge) { + this.edges.append(edge); + + return this; + } + + getNeighbors() { + const edges = this.edges.toArray(); + + const neighborsConverter = ({ value }) => { + return value.startVertex === this ? value.endVertex : value.startVertex; + }; + + // Return either start or end vertex. + // For undirected graphs it is possible that current vertex will be the end one. + return edges.map(neighborsConverter); + } + + /** + * @param requiredEdge {GraphEdge} + * @returns {boolean} + */ + hasEdge(requiredEdge) { + const edgeNode = this.edges.find({ + callback: edge => edge === requiredEdge, + }); + + return !!edgeNode; + } + + /** + * @param vertex {GraphVertex} + * @returns {boolean} + */ + hasNeighbor(vertex) { + const vertexNode = this.edges.find({ + callback: edge => edge.startVertex === vertex || edge.endVertex === vertex, + }); + + return !!vertexNode; + } + + findEdge(vertex) { + const edgeFinder = (edge) => { + return edge.startVertex === vertex || edge.endVertex === vertex; + }; + + const edge = this.edges.find({ callback: edgeFinder }); + + return edge ? edge.value : null; + } + + /** + * @returns {string} + */ + getKey() { + return this.value; + } + + /** + * @param callback {function} + * @returns {string} + */ + toString(callback) { + return callback ? callback(this.value) : `${this.value}`; + } +} diff --git a/src/data-structures/graph/__test__/Graph.test.js b/src/data-structures/graph/__test__/Graph.test.js new file mode 100644 index 00000000..8bc5eead --- /dev/null +++ b/src/data-structures/graph/__test__/Graph.test.js @@ -0,0 +1,114 @@ +import Graph from '../Graph'; +import GraphVertex from '../GraphVertex'; +import GraphEdge from '../GraphEdge'; + +describe('Graph', () => { + it('should add vertices to graph', () => { + const graph = new Graph(); + + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + + graph + .addVertex(vertexA) + .addVertex(vertexB); + + expect(graph.toString()).toBe('A,B'); + expect(graph.getVertexByKey(vertexA.getKey())).toEqual(vertexA); + expect(graph.getVertexByKey(vertexB.getKey())).toEqual(vertexB); + }); + + it('should add edges to undirected graph', () => { + const graph = new Graph(); + + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + + graph.addEdge(edgeAB); + + const graphVertexA = graph.findVertexByKey(vertexA.getKey()); + const graphVertexB = graph.findVertexByKey(vertexB.getKey()); + + expect(graph.toString()).toBe('A,B'); + expect(graphVertexA).toBeDefined(); + expect(graphVertexB).toBeDefined(); + + expect(graph.findVertexByKey('not existing')).toBeNull(); + + expect(graphVertexA.getNeighbors().length).toBe(1); + expect(graphVertexA.getNeighbors()[0]).toEqual(vertexB); + expect(graphVertexA.getNeighbors()[0]).toEqual(graphVertexB); + + expect(graphVertexB.getNeighbors().length).toBe(1); + expect(graphVertexB.getNeighbors()[0]).toEqual(vertexA); + expect(graphVertexB.getNeighbors()[0]).toEqual(graphVertexA); + }); + + it('should add edges to directed graph', () => { + const graph = new Graph(true); + + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + + graph.addEdge(edgeAB); + + const graphVertexA = graph.findVertexByKey(vertexA.getKey()); + const graphVertexB = graph.findVertexByKey(vertexB.getKey()); + + expect(graph.toString()).toBe('A,B'); + expect(graphVertexA).toBeDefined(); + expect(graphVertexB).toBeDefined(); + + expect(graphVertexA.getNeighbors().length).toBe(1); + expect(graphVertexA.getNeighbors()[0]).toEqual(vertexB); + expect(graphVertexA.getNeighbors()[0]).toEqual(graphVertexB); + + expect(graphVertexB.getNeighbors().length).toBe(0); + }); + + it('should find edge by vertices in undirected graph', () => { + const graph = new Graph(); + + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + + const edgeAB = new GraphEdge(vertexA, vertexB, 10); + + graph.addEdge(edgeAB); + + const graphEdgeAB = graph.findEdge(vertexA, vertexB); + const graphEdgeBA = graph.findEdge(vertexB, vertexA); + const graphEdgeAC = graph.findEdge(vertexB, vertexC); + + expect(graphEdgeAC).toBeNull(); + expect(graphEdgeAB).toEqual(edgeAB); + expect(graphEdgeBA).toEqual(edgeAB); + expect(graphEdgeAB.weight).toBe(10); + }); + + it('should find edge by vertices in directed graph', () => { + const graph = new Graph(true); + + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + + const edgeAB = new GraphEdge(vertexA, vertexB, 10); + + graph.addEdge(edgeAB); + + const graphEdgeAB = graph.findEdge(vertexA, vertexB); + const graphEdgeBA = graph.findEdge(vertexB, vertexA); + const graphEdgeAC = graph.findEdge(vertexB, vertexC); + + expect(graphEdgeAC).toBeNull(); + expect(graphEdgeBA).toBeNull(); + expect(graphEdgeAB).toEqual(edgeAB); + expect(graphEdgeAB.weight).toBe(10); + }); +}); diff --git a/src/data-structures/graph/__test__/GraphEdge.test.js b/src/data-structures/graph/__test__/GraphEdge.test.js new file mode 100644 index 00000000..e53dc792 --- /dev/null +++ b/src/data-structures/graph/__test__/GraphEdge.test.js @@ -0,0 +1,24 @@ +import GraphEdge from '../GraphEdge'; +import GraphVertex from '../GraphVertex'; + +describe('GraphEdge', () => { + it('should create graph edge with default weight', () => { + const startVertex = new GraphVertex('A'); + const endVertex = new GraphVertex('B'); + const edge = new GraphEdge(startVertex, endVertex); + + expect(edge.startVertex).toEqual(startVertex); + expect(edge.endVertex).toEqual(endVertex); + expect(edge.weight).toEqual(1); + }); + + it('should create graph edge with predefined weight', () => { + const startVertex = new GraphVertex('A'); + const endVertex = new GraphVertex('B'); + const edge = new GraphEdge(startVertex, endVertex, 10); + + expect(edge.startVertex).toEqual(startVertex); + expect(edge.endVertex).toEqual(endVertex); + expect(edge.weight).toEqual(10); + }); +}); diff --git a/src/data-structures/graph/__test__/GraphVertex.test.js b/src/data-structures/graph/__test__/GraphVertex.test.js new file mode 100644 index 00000000..4851215b --- /dev/null +++ b/src/data-structures/graph/__test__/GraphVertex.test.js @@ -0,0 +1,100 @@ +import GraphVertex from '../GraphVertex'; +import GraphEdge from '../GraphEdge'; + +describe('GraphVertex', () => { + it('should throw an error when trying to create vertex without value', () => { + let vertex = null; + + function createEmptyVertex() { + vertex = new GraphVertex(); + } + + expect(vertex).toBeNull(); + expect(createEmptyVertex).toThrow(); + }); + + it('should create graph vertex', () => { + const vertex = new GraphVertex('A'); + + expect(vertex).toBeDefined(); + expect(vertex.value).toBe('A'); + expect(vertex.toString()).toBe('A'); + expect(vertex.getKey()).toBe('A'); + expect(vertex.edges.toString()).toBe(''); + }); + + it('should add edges to vertex and check if it exists', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('A'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + vertexA.addEdge(edgeAB); + + expect(vertexA.hasEdge(edgeAB)).toBeTruthy(); + expect(vertexB.hasEdge(edgeAB)).toBeFalsy(); + }); + + it('should return vertex neighbors in case if current node is start one', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + const edgeAC = new GraphEdge(vertexA, vertexC); + vertexA + .addEdge(edgeAB) + .addEdge(edgeAC); + + expect(vertexB.getNeighbors()).toEqual([]); + + const neighbors = vertexA.getNeighbors(); + + expect(neighbors.length).toBe(2); + expect(neighbors[0]).toEqual(vertexB); + expect(neighbors[1]).toEqual(vertexC); + }); + + it('should return vertex neighbors in case if current node is end one', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + + const edgeBA = new GraphEdge(vertexB, vertexA); + const edgeCA = new GraphEdge(vertexC, vertexA); + vertexA + .addEdge(edgeBA) + .addEdge(edgeCA); + + expect(vertexB.getNeighbors()).toEqual([]); + + const neighbors = vertexA.getNeighbors(); + + expect(neighbors.length).toBe(2); + expect(neighbors[0]).toEqual(vertexB); + expect(neighbors[1]).toEqual(vertexC); + }); + + it('should check if vertex has specific neighbor', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + vertexA.addEdge(edgeAB); + + expect(vertexA.hasNeighbor(vertexB)).toBeTruthy(); + expect(vertexA.hasNeighbor(vertexC)).toBeFalsy(); + }); + + it('should edge by vertex', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + vertexA.addEdge(edgeAB); + + expect(vertexA.findEdge(vertexB)).toEqual(edgeAB); + expect(vertexA.findEdge(vertexC)).toBeNull(); + }); +});