Before you can run BFS, DFS, or any other graph algorithm, you need to decide how to store the graph in memory. That choice affects the time and space complexity of every operation you perform on it.
There are two standard representations: the adjacency list and the adjacency matrix. This tutorial covers both, when to use each, and how to handle directed, undirected, and weighted graphs. You should be familiar with Graphs Introduction and Arrays before reading this.
Most real-world graphs are sparse (far fewer edges than the maximum possible), so adjacency lists are the default choice. Use an adjacency matrix when you need O(1) edge lookups or the graph is dense.
Quick reference
Consider this graph:
A --- B
| |
C --- D --- E
Vertices: A, B, C, D, E — Edges: A-B, A-C, B-D, C-D, D-E
As an adjacency list
A → [B, C]
B → [A, D]
C → [A, D]
D → [B, C, E]
E → [D]
Each vertex stores a list of its neighbors.
As an adjacency matrix
A B C D E
A [ 0, 1, 1, 0, 0 ]
B [ 1, 0, 0, 1, 0 ]
C [ 1, 0, 0, 1, 0 ]
D [ 0, 1, 1, 0, 1 ]
E [ 0, 0, 0, 1, 0 ]
A 2D grid where matrix[i][j] = 1 means there’s an edge from vertex i to vertex j.
classGraph:
def __init__(self):
self.adjacency_list = {}
defadd_vertex(self, vertex):
if vertex notin self.adjacency_list:
self.adjacency_list[vertex] = []
defadd_edge(self, v1, v2):
self.add_vertex(v1)
self.add_vertex(v2)
self.adjacency_list[v1].append(v2)
self.adjacency_list[v2].append(v1) # remove for directeddefremove_edge(self, v1, v2):
self.adjacency_list[v1] = [v for v in self.adjacency_list[v1] if v != v2]
self.adjacency_list[v2] = [v for v in self.adjacency_list[v2] if v != v1]
defremove_vertex(self, vertex):
for neighbor in self.adjacency_list.get(vertex, []):
self.adjacency_list[neighbor] = [
v for v in self.adjacency_list[neighbor] if v != vertex
]
del self.adjacency_list[vertex]
defhas_edge(self, v1, v2):
return v2 in self.adjacency_list.get(v1, [])
defget_neighbors(self, vertex):
return self.adjacency_list.get(vertex, [])
defprint_graph(self):
for vertex, neighbors in self.adjacency_list.items():
print(f"{vertex} → {neighbors}")
g = Graph()
g.add_edge('A', 'B')
g.add_edge('A', 'C')
g.add_edge('B', 'D')
g.add_edge('C', 'D')
g.add_edge('D', 'E')
g.print_graph()
print(g.has_edge('A', 'B')) # Trueprint(g.has_edge('A', 'E')) # False
classGraphMatrix:
def __init__(self, vertices):
self.vertices = vertices
self.index = {v: i for i, v in enumerate(vertices)}
self.matrix = [[0] * len(vertices) for _ in range(len(vertices))]
defadd_edge(self, v1, v2):
i, j = self.index[v1], self.index[v2]
self.matrix[i][j] =1 self.matrix[j][i] =1# remove for directeddefhas_edge(self, v1, v2):
return self.matrix[self.index[v1]][self.index[v2]] ==1defget_neighbors(self, vertex):
i = self.index[vertex]
return [v for j, v in enumerate(self.vertices) if self.matrix[i][j] ==1]
defprint_graph(self):
print(' '+' '.join(self.vertices))
for i, v in enumerate(self.vertices):
print(f"{v}{self.matrix[i]}")
g = GraphMatrix(['A', 'B', 'C', 'D', 'E'])
g.add_edge('A', 'B')
g.add_edge('A', 'C')
g.add_edge('B', 'D')
g.add_edge('C', 'D')
g.add_edge('D', 'E')
g.print_graph()
print(g.has_edge('A', 'B')) # Trueprint(g.get_neighbors('D')) # ['B', 'C', 'E']
// Store weights instead of 0/1
// Use Infinity for no edge
constmatrix= [
// A B C D
[ 0, 4, 2, Infinity], // A
[ 4, 0, Infinity, 3 ], // B
[ 2, Infinity, 0, Infinity], // C
[Infinity, 3, Infinity, 0 ], // D
];
Weighted graphs are used in algorithms like Dijkstra’s shortest path, where edge weights represent distances or costs.
Directed graphs
In a directed graph (digraph), edges have a direction. The only change is that addEdge adds the connection in one direction only:
// Directed: A → B does NOT mean B → A
addEdge(from, to) {
this.addVertex(from);
this.addVertex(to);
this.adjacencyList.get(from).push(to);
// no reverse edge
}
For an adjacency matrix, matrix[i][j] = 1 means there’s an edge from i to j, but matrix[j][i] might be 0.
Adjacency list vs adjacency matrix
Operation
Adjacency List
Adjacency Matrix
Space
O(V + E)
O(V²)
Add edge
O(1)
O(1)
Remove edge
O(E)
O(1)
Check edge exists
O(degree)
O(1)
Get all neighbors
O(degree)
O(V)
Add vertex
O(1)
O(V²) — rebuild matrix
Where V = vertices, E = edges, and degree = number of edges on a vertex.
When to use which
Adjacency list when:
The graph is sparse (E « V²) — most real-world graphs
You frequently iterate over a vertex’s neighbors
Memory matters
Adjacency matrix when:
The graph is dense (E ≈ V²)
You need fast O(1) edge existence checks
The vertex count is small and fixed
A graph with 10,000 vertices needs a 10,000 × 10,000 matrix (100 million entries). If it only has 50,000 edges, an adjacency list uses a fraction of that memory.
Edge list
A third, simpler representation — just store edges as pairs:
Edge lists are compact and useful for algorithms like Kruskal’s minimum spanning tree that process edges directly. But checking if a specific edge exists requires scanning the entire list — O(E).
Complexity summary
Representation
Space
Check Edge
Get Neighbors
Add Edge
Adjacency List
O(V + E)
O(degree)
O(degree)
O(1)
Adjacency Matrix
O(V²)
O(1)
O(V)
O(1)
Edge List
O(E)
O(E)
O(E)
O(1)
Choose your representation based on the operations your algorithm needs most. For BFS and DFS, adjacency lists are almost always the right choice since both algorithms iterate over neighbors repeatedly.
Thanks for visiting
We are actively updating content to this site. Thanks for visiting! Please bookmark this page and visit again soon.