- A graph is a set of nodes (or vertices) connected by edges
- Any binary relationship can be represented by a graph
- Any social network, such as Facebook, Twitter and Google +, can be represented by a graph; Diagrams can also be used to represent roads, flights, communications, etc
Concept:
A graph G=(V, E) consists of the following elements:
- 5: A set of vertices
- E: A set of edges that connect vertices in V
Adjacent vertex: A vertex connected by an edge. If A and B are adjacent, A and E are not adjacent
Degree: the degree of A vertex is the number of its adjacent vertices. If A is connected to the other three vertices, the degree of A is 3
Path: a continuous sequence of vertices v 1, v2,..., vk, where vi and vi+1 are adjacent
Simple path: a simple path requires no duplicate vertices. A. D and G are a simple path; A ring is also a simple path
Directed graph:
- The edge of a directed graph has a direction
- - If there are paths between every two vertices in A graph, the graph is strongly connected. For example, C and D are strongly connected, while A and B are not strongly connected
- Weighted and unweighted graphs:
2.1 adjacency matrix
Graph is often implemented by adjacency matrix. Each node is associated with an integer that will be used as the index of the array.
Use a two-dimensional array to represent the connection between vertices. If the node with index I is adjacent to the node with index j, then array [i] [j] = = 1, otherwise array [i] [j] = = 0
Disadvantages:
- If a graph that is not strongly connected (sparse graph) is represented by an adjacency matrix, there will be many zeros in the matrix, which means that computer storage space will be wasted to represent nonexistent edges. For example, to find an adjacent vertex of a given vertex, even if the vertex has only one adjacent vertex, we have to iterate a whole row.
- The number of vertices in the graph may change, and the two-dimensional array is not flexible.
2.2 adjacency table
2.3 incidence matrix
In the incidence matrix, the rows of the matrix represent vertices and the list shows edges.
Incidence matrix is usually used when there are more edges than vertices to save space and memory.
3. Graph classSkeleton of declaration class:
class Graph { constructor(isDirected = false) { // The Graph constructor can receive a parameter to indicate whether the Graph is directed. By default, it is undirected this.isDirected = isDirected; // Use an array to store the names of all vertices in the graph this.vertices = []; // Use a dictionary to store adjacency tables this.adjList = new Dictionary(); } }
To add a new vertex to the graph:
addVertex(v) { // When receiving vertex v as a parameter and this vertex does not exist in the graph if (!this.vertices.includes(v)) { // Adds the vertex to the vertex list this.vertices.push(v); // In the adjacency table, set vertex v as the key, and the corresponding dictionary value is an empty array this.adjList.set(v, []); } };
To add edges between vertices:
addEdge(v, w) { // If vertex v does not exist in the graph, add it to the vertex list if (!this.adjList.get(v)) { this.addVertex(v); } // If the vertex w does not exist in the graph, add it to the vertex list if (!this.adjList.get(w)) { this.addVertex(w); } // Add an edge from vertex v to vertex W by adding w to the adjacency table of V this.adjList.get(v).push(w); if (!this.isDirected) { // Add an edge from w to v (based on undirected graph) this.adjList.get(w).push(v); } }
Declare the method to return the vertex list:
getVertices() { return this.vertices; }
Declare the method to return the adjacency table:
getAdjList() { return this.adjList; }
Test code:
const graph = new Graph(); const myVertices = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I']; for (let i = 0; i < myVertices.length; i++) { graph.addVertex(myVertices[i]); } graph.addEdge('A', 'B'); graph.addEdge('A', 'C'); graph.addEdge('A', 'D'); graph.addEdge('C', 'D'); graph.addEdge('C', 'G'); graph.addEdge('D', 'G'); graph.addEdge('D', 'H'); graph.addEdge('B', 'E'); graph.addEdge('B', 'F'); graph.addEdge('E', 'I'); console.log(graph.toString());
Implement the toString method of Graph class to output the Graph on the console:
toString() { let s = ''; for (let i = 0; i < this.vertices.length; i++) { s += `$ -> `; const neighbors = this.adjList.get(this.vertices[i]); for (let j = 0; j < neighbors.length; j++) { s += `$`; } s += '/n'; } return s; }
result:
Algorithm for traversing the graph:
- breadth first algorithm
- Depth first algorithm
Graph traversal can be used to find specific vertices or paths between two vertices, check whether the graph is connected, check whether the graph contains rings, etc.
The idea of graph traversal:
- The graph traversal algorithm must track each node visited for the first time, and track which nodes have not been fully explored
- Both algorithms need to specify the first vertex to be accessed
- Fully exploring a vertex requires viewing each edge of that vertex
- The vertices connected by each edge that have not been accessed are marked as discovered and added to the list of vertices to be accessed
- In order to ensure the efficiency of the algorithm, each vertex must be visited at most twice
- Each edge and vertex in the connected graph is accessed
Differences between the two algorithms:
algorithmdata structuredescribeDepth first algorithmStackThe vertices are stored in the stack. The vertices are explored along the path, and new adjacent vertices are accessedbreadth first algorithm queueThe vertices are stored in the queue, and the vertices that enter the queue first are explored firstWhen you want to label vertices that have been visited, you can use three colors to reflect their status:
- White: the vertex has not been accessed
- Gray: the vertex has been visited but not explored
- Black: the vertex has been visited and explored
You need to use the Colors variable as an enumerator:
const Colors = { WHITE: 0, GREY: 1, BLACK: 2 }
A helper is needed to help store whether vertices have been accessed. At the beginning of each algorithm, all vertices are marked as not accessed (white):
const initializeColor = vertices => { const color = {}; for (let i = 0; i < vertices.length; i++) { color[vertices[i]] = Colors.WHITE; } return color; }
4.1 breadth first search
The breadth first search algorithm will traverse the graph from the specified first vertex and access all its adjacent points (adjacent vertices) first, just like accessing one layer of the graph at a time, that is, access the vertices first wide and then deep.
Steps:
- Create a queue Q
- Mark v as found (gray) and put v into queue Q
- If Q is not empty, then:
- Dequeue u from Q
- Marked u as discovered (gray)
- Queue u all unreached neighbors (white)
- Marked u as explored (black)
Algorithm implementation:
const breadthFirstSearch = (graph, startVertex, callback) => { const vertices = graph.getVertices(); const adjList = graph.getAdjList(); // Initialize the color array to white with the initializeColor function const color = initializeColor(vertices); // Declare and create a queue to store vertices to be accessed and explored const enqueue = new Queue(); // Queue vertices queue.enqueue(startVertex); // If the queue is not empty while (!queue.isEmpty()) { // Remove a vertex from the queue through the out of queue operation const u = queue.dequeue(); // Gets an adjacency table containing all its neighbors const neighbors = adjList.get(u); // Marking the vertex gray indicates that the vertex was found but not explored const [u] = Colors.GREY; for (let i = 0; i < neighbors.length; i++) { // For each adjacent point of u, its value needs to be obtained const w = neighbors[i]; // If it hasn't been visited if (color[w] === Colors.WHITE) { // The label has found it and is set to gray color[w] = Colors.GREY; // Queue the vertex queue.enqueue(w); } } // After exploring the vertex and its adjacent vertices, mark the vertex as explored, that is, black color[u] = Colors.BLACK; // The breadthFirstSearch method receives a callback. This parameter is optional and will be used if a callback function is passed if (callback) { callback(u); } } }
Test code:
const printVertex = (value) => console.log('Visited vertex: ' + value); breadthFirstSearch(graph, myVertices[0], printVertex);
result:
1. Use BFS to find the shortest path
Given a graph G and source vertex v, find the shortest path distance between each vertex u and V.
For a given vertex v, the breadth first algorithm will first access all vertices with a distance of 1, then vertices with a distance of 2, and so on.
- Distance from v to u [u]
- The backtracking point predecessors[u] is used to derive the shortest path from v to each other vertex U
Improved breadth first approach:
const BFS = (graph, startVertex) => { const verteices = graph.getVertices(); const adjList = graph.getAdjList(); const color = initializeColor(verteices); const queue = new Queue(); // Represents the distance const distances = {}; // Represents a forward tracking point const predecessors = {}; queue.enqueue(startVertex); // For each vertex in the graph for(let i = 0; i < verteices.length; i++) { // Initialize with 0 and null distances[verteices[i]] = 0; predecessors[verteices[i]] = null; } while(!queue.isEmpty()) { const u = queue.dequeue(); const neighbors = adjList.get(u); color[u] = Colors.GREY; for (let i = 0; i < neighbors.length; i++) { const w = neighbors[i]; if (color[w] === CVolors.WHITE) { color[w] = Colors.GREY; // Increase the distance between v and w distances[w] = distances[u] + 1; // Set the forward tracking point of w to u predecessors[w] = u; queue.enqueue(w); } } color[u] = Colors.BLACK; } // Returns an object containing distances and predecessors return { distances, predecessors } }
4.2 depth first search
Start traversing the graph from the first specified vertex, follow the path until the last vertex of the path is accessed, and then back off the original path and explore the next path.
The depth first search algorithm does not require a source vertex.
In the depth first search algorithm, if the vertex v in the graph is not accessed, the vertex v is accessed.
Steps:
- Marked v as discovered (gray)
- For all unreachable (white) adjacent points w of v, access vertex W
- Marked v as explored (black)
// Depth first search // Receive a Graph class instance and callback function as parameters const depthFirstSearch = (graph, callback) => { const vertices = graph.getVertices(); const adjList = graph.getAdjList(); const color = initializeColor(vertices); // Call a private recursive function for each vertex in the instance that has not been accessed for (let i = 0; i < vertices.length; i++) { if (color[vertices[i]] === Colors.WHITE) { // The parameters passed are the vertex u, color array and callback function to be accessed depthFirstSearchVisit(vertices[i], color, adjList, callback); } } }; const depthFirstSearchVisit = (u, color, adjList, callback) => { // When you access vertex u, label it as discovered color[u] = Colors.GREY; // If there is a callback function, the function is executed to output the accessed vertices if (callback) { callback(u); } // Gets a list containing all adjacent points of vertex u const neighbors = adjList.get(u); // For each unreachable adjacent point w of vertex u, call the depthFirstSearchVisit function and pass W and other parameters for (let i = 0; i < neighbors.length; i++) { const w = neighbors[i]; if (color[w] === Colors.WHITE) { depthFirstSearchVisit(w, color, adjList, callback); } } color[u] = Colors.BLACK; }
If you want the depth first algorithm to traverse all nodes of graph G, build a "forest" and a set of source vertices, and output two arrays: discovery time and completion time.
- Discovery time of vertex u d[u]
- When the top point u is marked black, the completion time of u is f[u]
- Backtracking point p[u] of vertex u
const DFS = graph => { const vertices = graph.getVertices(); const adjList = graph.getAdjList(); const color = initializeColor(vertices); const d = {}; const f = {}; const p = {}; // Track discovery time and completion time const time = { const: 0 }; // Declaration d, f, p for (let i = 0; i < vertices.length; i++) { f[vertices[i]] = 0; d[vertices[i]] = 0; p[vertices[i]] = null; } for (let i = 0; i < vertices.length; i++) { if (color[vertices[i]] === Colors.WHITE) { DFSVisit(vertices[i], color, d, f, p, time, adjList); } } // Initializes the array for each vertex of the graph and returns these values at the end of the method return { discovery: d, finished: f, predecessors: p } }; const DFSVisit = (u, color, d, f, p, time, adjList) => { color[u] = Colors.GREY; // When a vertex is first discovered, track its discovery time d[u] = ++time.count; const neighbors = adjList.get(u); for (let i = 0; i < verteices.length; i++) { if (color[w] === Colors.WHITE) { // Tracking backtracking point p[w] = u; DFSVisit(w, color, d, f, p.time, adjList); } } color[u] = Colors.BLACK; // After the vertex is fully explored, track its completion time f[u] = ++time.count; }