|
| 1 | +# Graph Data Stucture |
| 2 | + |
| 3 | +Graph is a non-linear data structure consisting of vertices and edges. It is a powerful tool for representing and analyzing complex relationships between objects or entities. |
| 4 | + |
| 5 | +## Components of a Graph |
| 6 | + |
| 7 | +1. **Vertices:** Vertices are the fundamental units of the graph. Sometimes, vertices are also known as vertex or nodes. Every node/vertex can be labeled or unlabeled. |
| 8 | + |
| 9 | +2. **Edges:** Edges are drawn or used to connect two nodes of the graph. It can be ordered pair of nodes in a directed graph. Edges can connect any two nodes in any possible way. There are no rules. very edge can be labelled/unlabelled. |
| 10 | + |
| 11 | +## Basic Operations on Graphs |
| 12 | +- Insertion of Nodes/Edges in the graph |
| 13 | +- Deletion of Nodes/Edges in the graph |
| 14 | +- Searching on Graphs |
| 15 | +- Traversal of Graphs |
| 16 | + |
| 17 | +## Types of Graph |
| 18 | + |
| 19 | + |
| 20 | +**1. Undirected Graph:** In an undirected graph, edges have no direction, and they represent symmetric relationships between nodes. If there is an edge between node A and node B, you can travel from A to B and from B to A. |
| 21 | + |
| 22 | +**2. Directed Graph (Digraph):** In a directed graph, edges have a direction, indicating a one-way relationship between nodes. If there is an edge from node A to node B, you can travel from A to B but not necessarily from B to A. |
| 23 | + |
| 24 | +**3. Weighted Graph:** In a weighted graph, edges have associated weights or costs. These weights can represent various attributes such as distance, cost, or capacity. Weighted graphs are commonly used in applications like route planning or network optimization. |
| 25 | + |
| 26 | +**4. Cyclic Graph:** A cyclic graph contains at least one cycle, which is a path that starts and ends at the same node. In other words, you can traverse the graph and return to a previously visited node by following the edges. |
| 27 | + |
| 28 | +**5. Acyclic Graph:** An acyclic graph, as the name suggests, does not contain any cycles. This type of graph is often used in scenarios where a cycle would be nonsensical or undesirable, such as representing dependencies between tasks or events. |
| 29 | + |
| 30 | +**6. Tree:** A tree is a special type of acyclic graph where each node has a unique parent except for the root node, which has no parent. Trees have a hierarchical structure and are frequently used in data structures like binary trees or decision trees. |
| 31 | + |
| 32 | +## Representation of Graphs |
| 33 | +There are two ways to store a graph: |
| 34 | + |
| 35 | +1. **Adjacency Matrix:** |
| 36 | +In this method, the graph is stored in the form of the 2D matrix where rows and columns denote vertices. Each entry in the matrix represents the weight of the edge between those vertices. |
| 37 | + |
| 38 | +```python |
| 39 | +def create_adjacency_matrix(graph): |
| 40 | + num_vertices = len(graph) |
| 41 | + |
| 42 | + adj_matrix = [[0] * num_vertices for _ in range(num_vertices)] |
| 43 | + |
| 44 | + for i in range(num_vertices): |
| 45 | + for j in range(num_vertices): |
| 46 | + if graph[i][j] == 1: |
| 47 | + adj_matrix[i][j] = 1 |
| 48 | + adj_matrix[j][i] = 1 |
| 49 | + |
| 50 | + return adj_matrix |
| 51 | + |
| 52 | + |
| 53 | +graph = [ |
| 54 | + [0, 1, 0, 0], |
| 55 | + [1, 0, 1, 0], |
| 56 | + [0, 1, 0, 1], |
| 57 | + [0, 0, 1, 0] |
| 58 | +] |
| 59 | + |
| 60 | +adj_matrix = create_adjacency_matrix(graph) |
| 61 | + |
| 62 | +for row in adj_matrix: |
| 63 | + print(' '.join(map(str, row))) |
| 64 | + |
| 65 | +``` |
| 66 | + |
| 67 | +2. **Adjacency List:** |
| 68 | +In this method, the graph is represented as a collection of linked lists. There is an array of pointer which points to the edges connected to that vertex. |
| 69 | + |
| 70 | +```python |
| 71 | +def create_adjacency_list(edges, num_vertices): |
| 72 | + adj_list = [[] for _ in range(num_vertices)] |
| 73 | + |
| 74 | + for u, v in edges: |
| 75 | + adj_list[u].append(v) |
| 76 | + adj_list[v].append(u) |
| 77 | + |
| 78 | + return adj_list |
| 79 | + |
| 80 | +if __name__ == "__main__": |
| 81 | + num_vertices = 4 |
| 82 | + edges = [(0, 1), (0, 2), (1, 2), (2, 3), (3, 1)] |
| 83 | + |
| 84 | + adj_list = create_adjacency_list(edges, num_vertices) |
| 85 | + |
| 86 | + for i in range(num_vertices): |
| 87 | + print(f"{i} -> {' '.join(map(str, adj_list[i]))}") |
| 88 | +``` |
| 89 | +`Output` |
| 90 | +`0 -> 1 2` |
| 91 | +`1 -> 0 2 3` |
| 92 | +`2 -> 0 1 3` |
| 93 | +`3 -> 2 1 ` |
| 94 | + |
| 95 | + |
| 96 | + |
| 97 | +# Traversal Techniques |
| 98 | + |
| 99 | +## Breadth First Search (BFS) |
| 100 | +- It is a graph traversal algorithm that explores all the vertices in a graph at the current depth before moving on to the vertices at the next depth level. |
| 101 | +- It starts at a specified vertex and visits all its neighbors before moving on to the next level of neighbors. |
| 102 | +BFS is commonly used in algorithms for pathfinding, connected components, and shortest path problems in graphs. |
| 103 | + |
| 104 | +**Steps of BFS algorithms** |
| 105 | + |
| 106 | + |
| 107 | +- **Step 1:** Initially queue and visited arrays are empty. |
| 108 | +- **Step 2:** Push node 0 into queue and mark it visited. |
| 109 | +- **Step 3:** Remove node 0 from the front of queue and visit the unvisited neighbours and push them into queue. |
| 110 | +- **Step 4:** Remove node 1 from the front of queue and visit the unvisited neighbours and push them into queue. |
| 111 | +- **Step 5:** Remove node 2 from the front of queue and visit the unvisited neighbours and push them into queue. |
| 112 | +- **Step 6:** Remove node 3 from the front of queue and visit the unvisited neighbours and push them into queue. |
| 113 | +- **Step 7:** Remove node 4 from the front of queue and visit the unvisited neighbours and push them into queue. |
| 114 | + |
| 115 | +```python |
| 116 | + |
| 117 | +from collections import deque |
| 118 | + |
| 119 | +def bfs(adjList, startNode, visited): |
| 120 | + q = deque() |
| 121 | + |
| 122 | + visited[startNode] = True |
| 123 | + q.append(startNode) |
| 124 | + |
| 125 | + while q: |
| 126 | + currentNode = q.popleft() |
| 127 | + print(currentNode, end=" ") |
| 128 | + |
| 129 | + for neighbor in adjList[currentNode]: |
| 130 | + if not visited[neighbor]: |
| 131 | + visited[neighbor] = True |
| 132 | + q.append(neighbor) |
| 133 | + |
| 134 | +def addEdge(adjList, u, v): |
| 135 | + adjList[u].append(v) |
| 136 | + |
| 137 | +def main(): |
| 138 | + vertices = 5 |
| 139 | + |
| 140 | + adjList = [[] for _ in range(vertices)] |
| 141 | + |
| 142 | + addEdge(adjList, 0, 1) |
| 143 | + addEdge(adjList, 0, 2) |
| 144 | + addEdge(adjList, 1, 3) |
| 145 | + addEdge(adjList, 1, 4) |
| 146 | + addEdge(adjList, 2, 4) |
| 147 | + |
| 148 | + visited = [False] * vertices |
| 149 | + |
| 150 | + print("Breadth First Traversal", end=" ") |
| 151 | + bfs(adjList, 0, visited) |
| 152 | + |
| 153 | +if __name__ == "__main__": #Output : Breadth First Traversal 0 1 2 3 4 |
| 154 | + main() |
| 155 | + |
| 156 | +``` |
| 157 | + |
| 158 | +- **Time Complexity:** `O(V+E)`, where V is the number of nodes and E is the number of edges. |
| 159 | +- **Auxiliary Space:** `O(V)` |
| 160 | + |
| 161 | + |
| 162 | +## Depth-first search |
| 163 | + |
| 164 | +Depth-first search is an algorithm for traversing or searching tree or graph data structures. The algorithm starts at the root node (selecting some arbitrary node as the root node in the case of a graph) and explores as far as possible along each branch before backtracking. |
| 165 | + |
| 166 | +**Steps of DFS algorithms** |
| 167 | + |
| 168 | +- **Step 1:** Initially stack and visited arrays are empty. |
| 169 | +- **Step 2:** Visit 0 and put its adjacent nodes which are not visited yet into the stack. |
| 170 | +- **Step 3:** Now, Node 1 at the top of the stack, so visit node 1 and pop it from the stack and put all of its adjacent nodes which are not visited in the stack. |
| 171 | +- **Step 4:** Now, Node 2 at the top of the stack, so visit node 2 and pop it from the stack and put all of its adjacent nodes which are not visited (i.e, 3, 4) in the stack. |
| 172 | +- **Step 5:** Now, Node 4 at the top of the stack, so visit node 4 and pop it from the stack and put all of its adjacent nodes which are not visited in the stack. |
| 173 | +- **Step 6:** Now, Node 3 at the top of the stack, so visit node 3 and pop it from the stack and put all of its adjacent nodes which are not visited in the stack. |
| 174 | + |
| 175 | + |
| 176 | + |
| 177 | +```python |
| 178 | +from collections import defaultdict |
| 179 | + |
| 180 | +class Graph: |
| 181 | + |
| 182 | + def __init__(self): |
| 183 | + self.graph = defaultdict(list) |
| 184 | + |
| 185 | + def addEdge(self, u, v): |
| 186 | + self.graph[u].append(v) |
| 187 | + |
| 188 | + def DFSUtil(self, v, visited): |
| 189 | + visited.add(v) |
| 190 | + print(v, end=' ') |
| 191 | + |
| 192 | + for neighbour in self.graph[v]: |
| 193 | + if neighbour not in visited: |
| 194 | + self.DFSUtil(neighbour, visited) |
| 195 | + |
| 196 | + def DFS(self, v): |
| 197 | + visited = set() |
| 198 | + self.DFSUtil(v, visited) |
| 199 | + |
| 200 | +if __name__ == "__main__": |
| 201 | + g = Graph() |
| 202 | + g.addEdge(0, 1) |
| 203 | + g.addEdge(0, 2) |
| 204 | + g.addEdge(1, 2) |
| 205 | + g.addEdge(2, 0) |
| 206 | + g.addEdge(2, 3) |
| 207 | + g.addEdge(3, 3) |
| 208 | + |
| 209 | + print("Depth First Traversal (starting from vertex 2): ",g.DFS(2)) |
| 210 | + |
| 211 | +``` |
| 212 | +`Output: Depth First Traversal (starting from vertex 2): 2 0 1 3 ` |
| 213 | + |
| 214 | +- **Time complexity:** `O(V + E)`, where V is the number of vertices and E is the number of edges in the graph. |
| 215 | +- **Auxiliary Space:** `O(V + E)`, since an extra visited array of size V is required, And stack size for iterative call to DFS function. |
| 216 | + |
| 217 | +</br> |
| 218 | + |
| 219 | + |
0 commit comments