Graph Representations and Traversal Setup
Build directed and undirected graphs with adjacency lists, initialise BFS with a deque, and DFS with a stack or recursion, handling visited tracking.
Graph Representations and Traversal Setup is a free DSA Interview Prep lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DSA Interview Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What is a Graph?
A graph is a collection of nodes (vertices) connected by edges. Unlike trees, graphs can have cycles, multiple paths between nodes, and disconnected components. Graphs model real-world systems like social networks, road maps, dependency trees, and web page links. Almost every non-trivial system-design and algorithm interview touches graphs — mastering their representation and traversal is essential.
# Graph terminology:
# - V: set of vertices (nodes)
# - E: set of edges
# - Directed graph: edges have direction (A -> B but not B -> A)
# - Undirected graph: edges are bidirectional
# - Weighted graph: edges have costs/weights
# - Cyclic: contains at least one cycle
# - Acyclic: no cycles (DAG = Directed Acyclic Graph)
# - Connected: every node reachable from every other
# - Disconnected: multiple isolated components
print('Graph: nodes + edges, directed/undirected, weighted/unweighted')Adjacency List Representation
An adjacency list stores each node's list of neighbours. In Python, use a dict mapping each node to a list of adjacent nodes. This is the most common representation in interview problems: O(V + E) space (efficient for sparse graphs), O(degree) to iterate over neighbours, and O(1) average to check adjacency with a hash set variant. Most LeetCode graph problems use this format.
from collections import defaultdict
# Build an undirected graph
def build_undirected(edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # both directions
return graph
edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
graph = build_undirected(edges)
print(dict(graph))
# {0:[1,2], 1:[0,3], 2:[0,3], 3:[1,2,4], 4:[3]}
# Directed graph: only one direction
def build_directed(edges):
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v) # only u -> v
return graphAdjacency Matrix Representation
An adjacency matrix is a V×V 2D array where matrix[i][j] = 1 (or the edge weight) if there is an edge from i to j, and 0 otherwise. It offers O(1) edge lookup but uses O(V²) space regardless of edge count — wasteful for sparse graphs. It is preferred when the graph is dense (many edges) or when fast edge-existence checks are critical, such as in Floyd-Warshall all-pairs shortest paths.
# Adjacency matrix for 5 nodes
V = 5
matrix = [[0] * V for _ in range(V)]
edges = [(0,1), (0,2), (1,3), (2,3), (3,4)]
for u, v in edges:
matrix[u][v] = 1
matrix[v][u] = 1 # undirected
# Print the matrix:
for row in matrix:
print(row)
# Neighbour check: O(1)
print('Edge 0-2:', bool(matrix[0][2])) # True
print('Edge 0-4:', bool(matrix[0][4])) # False
# Space: O(V^2) vs adjacency list O(V+E)
# Dense graph: matrix often better; sparse: list betterEdge List Representation
An edge list is the simplest representation: just a list of (source, destination) tuples, optionally with weights. It uses O(E) space and is easy to iterate over all edges. However, finding a node's neighbours requires scanning all edges: O(E). Edge lists are used in graph algorithms that iterate over all edges exactly, such as Bellman-Ford (relax all edges n-1 times) and Kruskal's minimum spanning tree algorithm.
# Weighted edge list: (source, destination, weight)
edge_list = [
(0, 1, 4),
(0, 2, 1),
(1, 3, 1),
(2, 3, 5),
(3, 4, 3)
]
# Useful for:
# Bellman-Ford: iterate all edges n-1 times
# Kruskal's MST: sort by weight then union-find
# Sort by weight for Kruskal:
edge_list_sorted = sorted(edge_list, key=lambda e: e[2])
print('Sorted by weight:', edge_list_sorted)
# Finding neighbours: O(E) scan -- inefficient for traversal
node_0_neighbors = [v for u, v, w in edge_list if u == 0]
print('Node 0 neighbors:', node_0_neighbors)BFS Setup: Queue and Visited Set
BFS (Breadth-First Search) explores a graph level by level using a queue. The critical component is a visited set to avoid revisiting nodes in cyclic graphs. Without the visited set, BFS on a cyclic graph would loop forever. The standard setup: initialise the queue with the source node, mark it visited, then repeatedly dequeue, process, and enqueue unvisited neighbours.
from collections import deque
def bfs(graph, start):
visited = {start} # mark source as visited
queue = deque([start]) # initialise queue
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbour in graph[node]:
if neighbour not in visited:
visited.add(neighbour) # mark BEFORE enqueue
queue.append(neighbour)
return order
from collections import defaultdict
graph = defaultdict(list)
for u, v in [(0,1),(0,2),(1,3),(2,3),(3,4)]:
graph[u].append(v); graph[v].append(u)
print(bfs(graph, 0)) # [0, 1, 2, 3, 4]DFS Setup: Stack or Recursion
DFS (Depth-First Search) explores as far as possible along each branch before backtracking. Implement it recursively (using the call stack) or iteratively (using an explicit stack). Both require a visited set for cyclic graphs. The iterative version pushes neighbours in reverse order to match recursive DFS traversal order, though the order of exploration may vary between the two implementations.
def dfs_recursive(graph, node, visited=None, order=None):
if visited is None: visited = set(); order = []
visited.add(node)
order.append(node)
for neighbour in graph[node]:
if neighbour not in visited:
dfs_recursive(graph, neighbour, visited, order)
return order
def dfs_iterative(graph, start):
visited = set()
stack = [start]
order = []
while stack:
node = stack.pop()
if node in visited: continue
visited.add(node)
order.append(node)
for neighbour in reversed(graph[node]): # reverse for same order as recursive
if neighbour not in visited:
stack.append(neighbour)
return order
print('Recursive DFS:', dfs_recursive(graph, 0))
print('Iterative DFS:', dfs_iterative(graph, 0))When to Use BFS vs DFS
Choose BFS when you need the shortest path (fewest edges) in an unweighted graph, or when you need to process nodes level by level. Choose DFS when you need to explore all reachable nodes, detect cycles, find connected components, perform topological sort, or enumerate all paths. In practice: BFS for 'shortest/minimum hops', DFS for 'existence/reachability/enumeration'.
# BFS use cases:
# - Shortest path in unweighted graph (fewest edges)
# - Level-order traversal
# - Word ladder (minimum transformations)
# - Clone graph
# DFS use cases:
# - Connected components (flood fill)
# - Cycle detection
# - Topological sort
# - All paths between two nodes
# - Maze solving (any path)
# - N-queens, Sudoku (backtracking)
# Both: O(V + E) time, O(V) space for visited
print('BFS: shortest hops | DFS: existence and enumeration')Graph from LeetCode Input Formats
LeetCode graph problems come in different input formats. Edge list: [[0,1],[0,2]] — build adjacency list. Adjacency list index-based: graph[i] is the list of neighbours of i. Grid/matrix: an m×n 2D array where cells are nodes and adjacent cells (up/down/left/right) are neighbours. Node with children: custom classes like Node(val, neighbors). Recognise these and convert to adjacency list as your first step.
# Format 1: edge list -> adjacency list
def edges_to_adj(n, edges):
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
return graph
# Format 2: 2D grid -> adjacency (implicit)
# Neighbours of (r, c): (r-1,c), (r+1,c), (r,c-1), (r,c+1)
DIRS = [(-1,0),(1,0),(0,-1),(0,1)]
def grid_neighbours(grid, r, c):
rows, cols = len(grid), len(grid[0])
return [(r+dr, c+dc) for dr, dc in DIRS
if 0 <= r+dr < rows and 0 <= c+dc < cols]
grid = [[1,1,0],[0,1,1],[1,0,0]]
print('Neighbours of (0,0):', grid_neighbours(grid, 0, 0))
print('Neighbours of (1,1):', grid_neighbours(grid, 1, 1))Marking Visited in Grids
For grid problems, there are two ways to track visited cells. Option A: use a separate visited set of (row, col) tuples — O(m*n) extra space. Option B: modify the grid in-place by marking visited cells with a sentinel value (e.g., '#' or 2) and restore them afterward if needed. The in-place approach uses O(1) extra space and is common in flood-fill and number-of-islands problems.
def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols:
return
if grid[r][c] != '1':
return
grid[r][c] = '#' # mark as visited (in-place)
dfs(r+1, c); dfs(r-1, c)
dfs(r, c+1); dfs(r, c-1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
dfs(r, c)
count += 1
return count
grid = [['1','1','0','0'],
['1','1','0','0'],
['0','0','1','0'],
['0','0','0','1']]
print(num_islands(grid)) # 3Initialising BFS with Multiple Sources
Multi-source BFS starts from multiple nodes simultaneously by initialising the queue with all source nodes marked visited. This is used in problems like 'distance from nearest 0', 'rotting oranges', and 'walls and gates', where you want the shortest distance from any of the source nodes. Multi-source BFS runs in O(V + E) — the same as single-source — because each node is still visited at most once.
from collections import deque
def rotting_oranges(grid):
rows, cols = len(grid), len(grid[0])
queue = deque()
fresh = 0
# Multi-source: all rotten oranges start at time=0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
queue.append((r, c, 0)) # (row, col, time)
elif grid[r][c] == 1:
fresh += 1
dirs = [(0,1),(0,-1),(1,0),(-1,0)]
time = 0
while queue:
r, c, t = queue.popleft()
for dr, dc in dirs:
nr, nc = r+dr, c+dc
if 0<=nr<rows and 0<=nc<cols and grid[nr][nc]==1:
grid[nr][nc] = 2 # mark rotten
fresh -= 1
queue.append((nr, nc, t+1))
time = t + 1
return time if fresh == 0 else -1
print(rotting_oranges([[2,1,1],[1,1,0],[0,1,1]])) # 4Graph Density and Representation Choice
The choice between adjacency list and matrix depends on graph density — the ratio E/V². A sparse graph (E << V²) benefits from adjacency lists: O(V+E) space vs O(V²) for a matrix. A dense graph (E ≈ V²) benefits from adjacency matrices: O(1) edge lookup vs O(degree) for lists. For interview problems, adjacency lists are almost always the right choice since most problems involve sparse graphs.
# Graph density comparison:
# Sparse: social network (V=1B users, avg 200 friends)
# E = 200 * 1B = 200B << V^2 = 10^18 -> adjacency list
# Dense: complete graph (every node connected to every other)
# E = V*(V-1)/2 ≈ V^2 -> adjacency matrix
# Interview rule of thumb:
# - Default to adjacency list (defaultdict(list))
# - Use matrix only when asked about dense graph or O(1) edge lookup
# - Grid problems: use implicit adjacency (4-directional neighbours)
print('Sparse graph (E << V^2): use adjacency list')
print('Dense graph (E ~ V^2): consider adjacency matrix')Quick Check
Test your understanding of Data Structures & Algorithms — Coding Interview Prep concepts from this lesson.
Lesson Recap
In this lesson you learned: three graph representations (adjacency list, matrix, edge list) and when to choose each, BFS and DFS setup with visited sets to avoid infinite loops on cyclic graphs, and practical patterns like in-place grid marking and multi-source BFS. Next up we apply BFS to find shortest paths and level traversal.
Frequently asked questions
Is the “Graph Representations and Traversal Setup” lesson free?
Yes — the full text of “Graph Representations and Traversal Setup” is free to read here on the web, and the DSA Interview Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DSA Interview Prep course, upgrade to CoddyKit PRO.
What will I learn in “Graph Representations and Traversal Setup”?
Build directed and undirected graphs with adjacency lists, initialise BFS with a deque, and DFS with a stack or recursion, handling visited tracking. You practise DSA Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DSA Interview Prep?
No prior experience is required. DSA Interview Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Graph Representations and Traversal Setup” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DSA Interview Prep lesson?
Yes. Every DSA Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Graph Representations and Traversal Setup
- BFS: Shortest Path and Level Traversal
- DFS: Connected Components and Flood Fill
- Cycle Detection in Directed and Undirected Graphs