Back to Data Structures & Algorithms
2026-02-238 min read

Graphs (Data Structures & Algorithms)

Learn Graphs (Data Structures & Algorithms) step by step with clear examples and exercises.

Why This Matters

Graphs are essential data structures that play a significant role in various fields such as computer science, mathematics, and social sciences. They help model and analyze complex systems, optimize routes, and predict trends. Understanding graphs is crucial for solving real-world problems like finding the shortest path between cities, detecting communities in social networks, or analyzing web page relationships. Moreover, mastering graph algorithms can prepare you for interviews and exams that require problem-solving skills using graph algorithms.

Prerequisites

To fully grasp this lesson, you should have a solid understanding of Python programming basics, including data structures like lists and dictionaries. Familiarity with common algorithms such as Depth-First Search (DFS), Breadth-First Search (BFS), and basic sorting algorithms will also be helpful.

Core Concept

A graph is a collection of nodes (also called vertices) connected by edges. Each edge connects a pair of nodes, and the graph can be either directed or undirected. In an undirected graph, edges are bidirectional, meaning that there is no distinction between (a, b) and (b, a). In contrast, in a directed graph, edges have a specific direction, so (a, b) and (b, a) represent different edges.

Adjacency List Representation

One common way to represent graphs is through an adjacency list. An adjacency list for an undirected graph consists of a list of lists, where each inner list represents the neighbors of a node. For example:

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

In this example, the graph has 5 nodes (A, B, C, D, E) and edges between them as specified in the adjacency list.

Adjacency Matrix Representation

Another way to represent a graph is through an adjacency matrix. An adjacency matrix for a graph with n nodes is a square matrix of size n x n, where each entry represents whether there exists an edge between the corresponding pair of nodes:

  1. If there's an edge between nodes i and j, then the value at position (i, j) is 1.
  2. If there's no edge between nodes i and j, then the value at position (i, j) is 0.

For example, consider the same graph as before:

graph_matrix = [
[0, 1, 1, 1, 0],
[1, 0, 1, 1, 1],
[1, 1, 0, 1, 0],
[1, 1, 1, 0, 0],
[0, 1, 0, 0, 0]
]

In this example, the graph matrix represents the same graph as the adjacency list.

Graph Traversal Algorithms

Graph traversal algorithms are used to explore and analyze graphs by visiting each node in the graph. The two most common graph traversal algorithms are Depth-First Search (DFS) and Breadth-First Search (BFS).

Depth-First Search (DFS)

DFS is a recursive algorithm that explores as far as possible along each branch before backtracking. DFS can be used to detect cycles in a graph, find connected components, or solve problems like the graph coloring problem.

def dfs(graph, node, visited=None):
if visited is None:
visited = set()

visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(graph, neighbor, visited)

Worked Example

graph = {

'A': ['B', 'C'],

'B': ['A', 'D', 'E'],

'C': ['A', 'D'],

'D': ['B', 'C'],

'E': ['B']

}

dfs(graph, 'A') # Traverse the graph starting from node A


#### Breadth-First Search (BFS)

BFS is an iterative algorithm that explores nodes in levels, visiting the closest nodes first. BFS can be used to find the shortest path between two nodes in an unweighted graph or a weighted graph where all edges have the same weight.

def bfs(graph, start_node):

queue = [(start_node, [start_node])] # Tuple containing node and path to that node

visited = set()

while queue:

current_node, path = queue.pop(0)

visited.add(current_node)

for neighbor in graph[current_node]:

if neighbor not in visited:

queue.append((neighbor, path + [neighbor]))

Worked Example

graph = {

'A': ['B', 'C'],

'B': ['A', 'D', 'E'],

'C': ['A', 'D'],

'D': ['B', 'C'],

'E': ['B']

}

bfs(graph, 'A') # Traverse the graph starting from node A using BFS

Worked Example

Let's explore a simple problem using graphs: finding the shortest path between two nodes in an undirected graph. We will use Dijkstra's algorithm to solve this problem.

def dijkstra(graph, start_node):
import heapq

distances = {node: float('inf') for node in graph}
distances[start_node] = 0
shortest_path = {}
unvisited_nodes = set(graph)

while unvisited_nodes:
current_node = min(unvisited_nodes, key=distances.get)
unvisited_nodes.remove(current_node)

for neighbor in graph[current_node]:
distance = distances[current_node] + 1
if distance < distances.get(neighbor, float('inf')):
distances[neighbor] = distance
shortest_path[neighbor] = current_node

return distances, shortest_path

graph = {
'A': {'B': 1, 'C': 4},
'B': {'A': 1, 'D': 2, 'E': 5},
'C': {'A': 4, 'D': 3},
'D': {'B': 2, 'C': 3, 'E': 1},
'E': {'B': 5, 'D': 1}
}

distances, shortest_path = dijkstra(graph, 'A')
print("Shortest distances:", distances)
print("Shortest path:", shortest_path)

In this example, we define a simple graph with weighted edges and use Dijkstra's algorithm to find the shortest path from node A to every other node. The output will show the shortest distances between nodes and the shortest paths found by the algorithm.

Common Mistakes

  1. Misunderstanding the difference between directed and undirected graphs: Remember that in an undirected graph, edges are bidirectional, while in a directed graph, edges have a specific direction.
  2. Not initializing all distances to float('inf'): Make sure to initialize all distances to float('inf') except for the starting node's distance, which should be set to 0.
  3. Forgetting to update shortest paths: Keep track of the shortest path between nodes as you traverse the graph to avoid getting stuck in infinite loops or finding incorrect paths.
  4. Incorrect priority queue implementation: Use a min-heap for Dijkstra's algorithm, since we need to find the node with the smallest distance at each step.
  5. Not handling unconnected components: If your graph has multiple disconnected components, make sure to call Dijkstra's algorithm on each component separately to get the correct shortest paths.
  6. Incorrectly implementing DFS or BFS: Ensure that you correctly implement DFS and BFS by visiting all nodes in the graph and handling cycles (for DFS) or unweighted graphs (for BFS).
  7. Not considering negative edge weights: Be aware that negative-weight cycles can cause issues in Dijkstra's algorithm. To handle these cases, consider using Bellman-Ford or Floyd-Warshall algorithms instead.

Practice Questions

  1. Implement Breadth-First Search (BFS) using an adjacency list representation of a graph.
  2. Given an undirected graph with weighted edges, use Dijkstra's algorithm to find the shortest path between two nodes.
  3. Write a function to detect cycles in an undirected graph using Depth-First Search (DFS).
  4. Implement Prim's algorithm for finding the minimum spanning tree of a graph.
  5. Given a directed acyclic graph (DAG), find the topological sort order of its nodes.
  6. Given a weighted undirected graph, use Bellman-Ford algorithm to find the shortest path between two nodes.
  7. Implement Floyd-Warshall algorithm for finding the shortest paths between all pairs of nodes in a weighted graph.
  8. Write a function to find strongly connected components in a directed graph using Kosaraju's algorithm.
  9. Given a graph with self-loops, modify Dijkstra's algorithm to handle them correctly.
  10. Implement A\* search algorithm for finding the shortest path between two nodes in a weighted graph with heuristics.

FAQ

What is the time complexity of Dijkstra's algorithm?

  • The time complexity of Dijkstra's algorithm is O(E log V), where E is the number of edges and V is the number of vertices.

Can I use Dijkstra's algorithm for weighted graphs with negative edge weights?

  • Yes, but be aware that negative-weight cycles can cause issues in Dijkstra's algorithm. To handle these cases, consider using Bellman-Ford or Floyd-Warshall algorithms instead.

How do I represent a directed graph using an adjacency list?

  • A directed graph can be represented using an adjacency list where each inner list contains the neighbors of a node, and edges are directional (e.g., [neighbor1, neighbor2] instead of ['neighbor1', 'neighbor2']).

How do I represent a graph using an adjacency matrix?

  • A graph can be represented using an adjacency matrix as a square matrix of size n x n, where each entry represents whether there exists an edge between the corresponding pair of nodes (1 for an edge, 0 for no edge).

What is the difference between Dijkstra's algorithm and Breadth-First Search (BFS)?

  • Both Dijkstra's algorithm and BFS are used to find shortest paths in a graph, but they differ in their applications: Dijkstra's algorithm is used for finding the shortest path from a single source node to all other nodes in a weighted graph, while BFS is used for finding the shortest path between any two nodes in an unweighted graph (or a weighted graph where all edges have the same weight).

What is the time complexity of Breadth-First Search (BFS)?

  • The time complexity of BFS is O(V + E), where V is the number of vertices and E is the number of edges.

How do I find strongly connected components in a graph?

  • One common algorithm for finding strongly connected components is Kosaraju's algorithm, which involves performing two depth-first searches on the graph.

What is the time complexity of Floyd-Warshall algorithm?

  • The time complexity of Floyd-Warshall algorithm is O(V^3), where V is the number of vertices.

How do I handle self-loops in Dijkstra's algorithm?

  • To handle self-loops in Dijkstra's algorithm, you can initialize the distance of each node to itself as 0 instead of using float('inf').

What is the difference between A\* search and Dijkstra's algorithm?

  • Both A\ search and Dijkstra's algorithm are used for finding shortest paths in a weighted graph, but they differ in how they handle heuristics: A\ search uses an admissible heuristic to estimate the cost of the shortest path from the current node to the goal node, while Dijkstra's algorithm does not use any heuristic.
Graphs (Data Structures & Algorithms) | Data Structures & Algorithms | XQA Learn