Graph Representation
Graphs model relationships between entities. The choice of representation — adjacency list or adjacency matrix — significantly impacts the time and space complexity of graph algorithms. Understanding both is essential for solving graph problems efficiently.
Graph Terminology
A graph G = (V, E) consists of a set of vertices (nodes) V and a set of edges E connecting pairs of vertices.
Key properties: - Directed (Digraph) — Edges have direction (A -> B does not imply B -> A). Models: web links, dependencies, social media following. - Undirected — Edges have no direction (A -- B implies B -- A). Models: friendships, roads, networks. - Weighted — Edges have associated weights/costs. Models: distances, latencies, capacities. - Degree — Number of edges connected to a vertex. In directed graphs: in-degree and out-degree. - Path — A sequence of vertices connected by edges. - Cycle — A path that starts and ends at the same vertex. - Connected — Every vertex is reachable from every other vertex (undirected). - DAG — Directed Acyclic Graph. No cycles. Used for dependency resolution, topological sort.
Adjacency List vs Adjacency Matrix
Two primary ways to represent a graph:
- Adjacency List — Array/dict of lists. Each vertex stores a list of its neighbors. Space: O(V + E). Edge lookup: O(degree). Best for sparse graphs (few edges).
- Adjacency Matrix — V x V 2D array. matrix[i][j] = 1 (or weight) if edge exists. Space: O(V^2). Edge lookup: O(1). Best for dense graphs (many edges).
- Most real-world graphs are sparse (social networks, road maps) — use adjacency lists.
- Use adjacency matrix when you need O(1) edge existence checks or the graph is dense.
Graph Representation in Python
Implementing both representations with weighted and unweighted graphs:
from collections import defaultdict
# ---- Adjacency List (most common) ----
class Graph:
def __init__(self, directed=False):
self.adj = defaultdict(list)
self.directed = directed
def add_edge(self, u, v, weight=1):
self.adj[u].append((v, weight))
if not self.directed:
self.adj[v].append((u, weight))
def neighbors(self, v):
return self.adj[v]
def vertices(self):
return list(self.adj.keys())
def __repr__(self):
return '
'.join(f"{v}: {neighbors}" for v, neighbors in self.adj.items())
# Undirected weighted graph
g = Graph(directed=False)
g.add_edge('A', 'B', 4)
g.add_edge('A', 'C', 2)
g.add_edge('B', 'C', 1)
g.add_edge('B', 'D', 5)
g.add_edge('C', 'D', 8)
print(g)
# ---- Adjacency Matrix ----
class GraphMatrix:
def __init__(self, n):
self.n = n
self.matrix = [[0] * n for _ in range(n)]
def add_edge(self, u, v, weight=1):
self.matrix[u][v] = weight
self.matrix[v][u] = weight # Undirected
def has_edge(self, u, v):
return self.matrix[u][v] != 0
def print_matrix(self):
for row in self.matrix:
print(row)
gm = GraphMatrix(4)
gm.add_edge(0, 1, 4)
gm.add_edge(0, 2, 2)
gm.add_edge(1, 3, 5)
gm.print_matrix()
# ---- Edge List (simple, for algorithms like Kruskal's) ----
edges = [
(4, 'A', 'B'), # (weight, u, v)
(2, 'A', 'C'),
(1, 'B', 'C'),
(5, 'B', 'D'),
]
edges.sort() # Sort by weight for Kruskal's MSTBuilding Graphs from Input
Common patterns for constructing graphs from problem input:
# ---- From edge list ----
def build_graph(n, edges):
"""n vertices, edges as list of [u, v] pairs"""
adj = defaultdict(list)
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
return adj
# ---- From grid (2D matrix) ----
def grid_to_graph(grid):
"""Convert a 2D grid to adjacency list"""
rows, cols = len(grid), len(grid[0])
adj = defaultdict(list)
directions = [(0,1),(0,-1),(1,0),(-1,0)]
for r in range(rows):
for c in range(cols):
if grid[r][c] != '#': # Not a wall
for dr, dc in directions:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != '#':
adj[(r,c)].append((nr,nc))
return adj
# ---- Detect if graph is bipartite (2-colorable) ----
def is_bipartite(adj, n):
color = {}
for start in range(n):
if start in color:
continue
queue = [start]
color[start] = 0
while queue:
node = queue.pop(0)
for neighbor in adj[node]:
if neighbor not in color:
color[neighbor] = 1 - color[node]
queue.append(neighbor)
elif color[neighbor] == color[node]:
return False
return TrueKey Takeaways
- Adjacency lists use O(V+E) space and are best for sparse graphs; matrices use O(V^2) for dense graphs.
- Directed graphs model one-way relationships; undirected model bidirectional connections.
- Most real-world graphs (social networks, maps) are sparse — use adjacency lists by default.
- Edge lists are useful for algorithms that process all edges (Kruskal's MST, Bellman-Ford).
- Grid problems are graph problems — convert the 2D grid to an adjacency list with 4-directional neighbors.
Contact Us
Have a question or feedback? Fill out the form below or reach us directly at support@nvaitraining.com