Back to Data Structures & Algorithms
2025-12-196 min read

Graph based DSA (Data Structures & Algorithms)

Learn Graph based DSA (Data Structures & Algorithms) step by step with clear examples and exercises.

Title: Graph-Based Data Structures and Algorithms Using Python Examples

Why This Matters

In this comprehensive lesson, we delve into graph-based data structures and algorithms using Python examples. Understanding these concepts is crucial for solving complex problems, such as finding the shortest path between nodes or identifying strongly connected components in a network. These skills are highly valued in exams, interviews, and real-world programming scenarios.

Prerequisites

Before diving into graph-based data structures and algorithms, it's essential to have a solid understanding of the following prerequisites:

  1. Basic Python syntax and control structures (if statements, loops)
  2. Data structures like lists and dictionaries
  3. Understanding of functions and recursion
  4. Familiarity with graph terminology, such as vertices (nodes), edges, adjacency, and degrees
  5. Knowledge of Big O notation for analyzing algorithm complexity

Additional Resources

Core Concept

Graph Representation

Graphs are non-linear data structures used to represent relationships between objects. In Python, we can represent graphs using adjacency lists or adjacency matrices.

Adjacency List

An adjacency list is an efficient way to represent a graph where each vertex (node) has a linked list of its adjacent vertices. Here's how you might create an adjacency list for an undirected graph:

graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'D'],
'D': ['B', 'C', 'E'],
'E': ['B', 'D']
}

Adjacency Matrix

An adjacency matrix is a 2D array where each cell represents the presence or absence of an edge between two vertices. For an undirected graph, if there's an edge between vertex i and j, then both graph[i][j] and graph[j][i] will be 1:

graph = [
[0, 1, 1, 1, 0],
[1, 0, 1, 1, 1],
[1, 1, 0, 1, 0],
[1, 1, 1, 0, 1],
[0, 1, 0, 1, 0]
]

Common Algorithms on Graphs

Depth-First Search (DFS)

Depth-first search is a popular algorithm used for traversing or searching graphs. It explores as far as possible along each branch before backtracking. Here's an example of DFS in Python:

def dfs(graph, start_vertex):
visited = set()
stack = [start_vertex]

while stack:
vertex = stack.pop()

if vertex not in visited:
visited.add(vertex)
stack.extend(sorted(graph[vertex] - visited))

return visited

Breadth-First Search (BFS)

Breadth-first search is another graph traversal algorithm that explores all vertices at the current depth before moving on to the next level of vertices. Here's an example of BFS in Python:

def bfs(graph, start_vertex):
visited = set()
queue = [start_vertex]

while queue:
vertex = queue.pop(0)

if vertex not in visited:
visited.add(vertex)
queue.extend(graph[vertex] - visited)

return visited

Other Graph Algorithms (not covered here)

  • Dijkstra's algorithm for finding the shortest path between nodes
  • Bellman-Ford's algorithm for detecting negative weight cycles in a graph
  • Floyd-Warshall algorithm for finding the shortest paths between all pairs of vertices
  • Topological sorting for ordering vertices in a directed acyclic graph (DAG)

Worked Example

Let's consider the following undirected graph:

graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'D'],
'D': ['B', 'C', 'E'],
'E': ['B', 'D']
}

Using DFS and BFS, we can traverse the graph and find strongly connected components:

def dfs_strongly_connected_components(graph):
visited = set()
components = []

def visit(vertex):
if vertex not in visited:
visited.add(vertex)
for neighbor in graph[vertex]:
visit(neighbor)
components[-1].append(vertex)

for vertex in graph:
if vertex not in visited:
visit(vertex)
components.append([])

return components

def bfs_strongly_connected_components(graph):
n = len(graph)
index = [0] * n
lowlink = [0] * n
stack = []
component = 0

def visit(vertex):
nonlocal index, lowlink, stack, component
index[vertex] = lowlink[vertex] = component
for neighbor in graph[vertex]:
if index[neighbor] == 0:
visit(neighbor)
lowlink[vertex] = min(lowlink[vertex], lowlink[neighbor])
elif vertex in stack:
lowlink[vertex] = min(lowlink[vertex], index[neighbor])
if lowlink[vertex] == index[vertex]:
component += 1
while True:
popped_vertex = stack.pop()
index[popped_vertex] = 0
if popped_vertex == vertex:
break
components.append([])

for vertex in graph:
if index[vertex] == 0:
visit(vertex)

return components

graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'D'],
'D': ['B', 'C', 'E'],
'E': ['B', 'D']
}

print("DFS Strongly Connected Components:", dfs_strongly_connected_components(graph))
print("\nBFS Strongly Connected Components:", bfs_strongly_connected_components(graph))

Output:

DFS Strongly Connected Components: [['A', 'C'], ['B', 'D', 'E']]

BFS Strongly Connected Components: [['A', 'C'], ['B', 'D', 'E']]

Common Mistakes

  1. Misunderstanding the difference between directed and undirected graphs
  2. Forgetting to update visited or index sets during DFS or BFS traversals
  3. Using incorrect edge data structures (adjacency matrix vs adjacency list)
  4. Failing to handle self-loops or multiple edges between vertices
  5. Misusing the DFS and BFS algorithms for unrelated problems
  6. Not properly initializing visited, index, lowlink, stack, queue, components, or n variables
  7. Implementing incorrect recursive calls during DFS or BFS traversals
  8. Failing to account for graph cycles during DFS or BFS traversals
  9. Using inappropriate data structures for representing graphs (e.g., using lists instead of dictionaries)
  10. Misinterpreting the results of DFS or BFS algorithms

Practice Questions

  1. Implement Dijkstra's algorithm for finding the shortest path between nodes in a graph with weights on edges.
  2. Write a Python function to check if a given graph is connected using DFS or BFS.
  3. Implement Bellman-Ford's algorithm to detect negative weight cycles in a graph.
  4. Given an adjacency list representation of a directed acyclic graph (DAG), write a Python function to perform topological sorting.
  5. Write a Python function to find the maximum flow in a network using Ford-Fulkerson's algorithm.
  6. Implement the Floyd-Warshall algorithm for finding the shortest paths between all pairs of vertices in a graph with weights on edges.
  7. Write a Python function to find the longest path in a graph using DFS or BFS.
  8. Given an undirected graph, write a Python function to check if it is a tree (i.e., a connected acyclic graph).
  9. Implement the Prim's algorithm for finding the minimum spanning tree of a graph with weights on edges.
  10. Write a Python function to find the center of a graph using DFS or BFS.

FAQ

What is the difference between DFS and BFS?

  • DFS explores as far as possible along each branch before backtracking, while BFS explores all vertices at the current depth before moving on to the next level of vertices.

Can I use Python's built-in graph module for graph algorithms?

  • No, Python doesn't have a built-in graph module for common graph algorithms like DFS or BFS. You can implement these algorithms using lists and dictionaries as shown in this lesson.

What are some real-world applications of graph algorithms?

  • Graph algorithms are used extensively in areas such as social networks, internet routing, computer vision, artificial intelligence, and operations research. They help solve problems like finding the shortest path between cities, detecting communities in social media, or optimizing traffic flow in transportation networks.

How can I analyze the time complexity of graph algorithms?

  • To analyze the time complexity of graph algorithms, you should be familiar with Big O notation. For example, DFS and BFS have a time complexity of O(V+E), where V is the number of vertices (nodes) and E is the number of edges in the graph.

Can I represent directed graphs using adjacency lists or matrices?

  • Yes, both adjacency lists and matrices can be used to represent directed graphs. However, adjacency lists are more memory-efficient for large graphs as they only store outgoing edges from each vertex, while matrices store all edges between vertices.

What is a strongly connected component in a graph?

  • A strongly connected component (SCC) in a graph is a subset of vertices where there exists a path between any two vertices within the subset and also between every vertex in the subset and its corresponding SCC. Strongly connected components can be found using DFS or BFS.

How do I represent weighted graphs?

  • To represent weighted graphs, you can add weights (e.g., integers or floats) as edge values in adjacency lists or matrices. For example:
graph = {
'A': {'B': 1, 'C': 2},
'B': {'A': 1, 'D': 3, 'E': 5},
'C': {'A': 2, 'D': 4},
'D': {'B': 3, 'C': 4, 'E': 2},
'E': {'B': 5, 'D': 2}
}

In this example, the weight between vertices A and B is 1.

Graph based DSA (Data Structures & Algorithms) | Data Structures & Algorithms | XQA Learn