Graph Representations
How to store a graph in code — adjacency list versus adjacency matrix — and how to pick the right one for the job.
What you'll learn
- What a graph is, and why it appears everywhere — social networks, build systems, maps
- The difference between directed and undirected graphs, and what 'degree' means
- How an adjacency list stays O(V+E), which is why sparse graphs love it
- How an adjacency matrix buys O(1) edge lookup at the price of O(V²) space
Before you start
A graph is the simplest structure for capturing relationships: just nodes (things) and edges (connections between them). Friendships on a social network, links between web pages, flights between airports — every one of these is a graph.
The question this lesson answers is not what a graph is, but how to store one so your code can work with it without drowning in memory or time.
A few words first
An edge can be directed (an arrow from A to B that does not imply B to A — a Twitter follow) or undirected (a mutual link — a Facebook friendship). It can also be weighted, carrying a number like distance or cost. A node’s degree is how many edges touch it. And graphs are usually sparse: a social network of a million people has nowhere near the trillion friendships a fully-connected graph would hold. That sparseness is the single fact that decides which representation to use.
Here is the small undirected graph we will store two ways:
Adjacency list
The everyday choice. For each node, you keep the list of its neighbours:
graph = {
"A": ["B", "C"],
"B": ["A", "D", "E"],
"C": ["A", "D"],
"D": ["B", "C", "E"],
"E": ["B", "D"],
}
This costs O(V + E) space — one entry per node, plus one per edge end. Walking all of a node’s neighbours is O(degree), which is exactly what graph traversals do over and over. Checking whether a specific edge exists is O(degree) with a list (or O(1) if you store neighbours in a set). Adding an edge is O(1).
Adjacency matrix
The other choice is a V×V grid where cell [i][j] = 1 marks an edge from node i to node j. For an undirected graph it is symmetric:
A B C D E
A 0 1 1 0 0
B 1 0 0 1 1
C 1 0 0 1 0
D 0 1 1 0 1
E 0 1 0 1 0
The matrix’s one superpower is O(1) edge lookup — “is there an edge from C to D?” is a single cell read. But it always costs O(V²) space, whether the graph has six edges or six million, and walking a node’s neighbours means scanning a whole row of mostly zeros — O(V). With 10,000 nodes that is 100 million cells, regardless of how few edges you actually have.
Which one?
| Your situation | Reach for |
|---|---|
| Sparse graph (E much smaller than V²) | Adjacency list |
| Constant “is there an edge?” checks | Matrix |
| BFS, DFS, Dijkstra on a large graph | Adjacency list |
| V small and the graph dense | Either |
| All-pairs algorithms (Floyd-Warshall) | Matrix |
The rule of thumb is short: start with an adjacency list. Almost every real graph — web, social, road, dependency — is sparse, so the matrix’s O(V²) is a memory tax you do not want to pay. Build one from an edge list and the cost difference shows up immediately:
def build_adj_list(edges):
graph = {}
for u, v in edges:
graph.setdefault(u, []).append(v)
graph.setdefault(v, []).append(u) # undirected: store both ends
return graph
edges = [("A","B"), ("A","C"), ("B","D"), ("B","E"), ("C","D"), ("D","E")]
graph = build_adj_list(edges)
V, E = len(graph), len(edges)
print("V =", V, " E =", E)
print("adjacency list : V + E =", V + E, "entries")
print("matrix would be : V × V =", V * V, "cells")
V = 5 E = 6
adjacency list : V + E = 11 entries
matrix would be : V × V = 25 cells
Eleven against twenty-five looks minor here. Scale to a million sparse nodes and it becomes the gap between megabytes and terabytes.
Practice
Quick check
Practice this in an interview
All questionsChoose a list when order matters and you need indexed access or duplicates. Choose a dict when you need to map keys to values and look up by key in O(1). Choose a set when you need uniqueness, fast membership testing, or set-algebra operations. Getting this choice wrong usually means either incorrect results (keeping duplicates when you needed uniqueness) or avoidable O(n) lookups.
Match the chart to the relationship in the data: comparison across categories calls for bars, trends over continuous time call for lines, correlation between two numeric variables calls for a scatter plot, and distribution shape calls for a histogram or box plot. The question you are answering — not aesthetics — drives the choice.
A state-space model carries a fixed-size hidden state forward through the sequence like a selective recurrence, giving O(N) time and constant per-step memory with no KV cache that grows with context. Attention instead compares every token to every other, which is O(N^2) but allows exact lookup of any past token. Mamba's gates are input-dependent, recovering much of attention's content-awareness; the trade-off is that a fixed state can't recall arbitrary far-back tokens as precisely. In practice, hybrids that interleave Mamba layers with a few attention layers give near-linear cost with near-attention quality.
Python lists are heterogeneous, pointer-based, and general-purpose. NumPy arrays are homogeneous, stored as contiguous typed memory, and support vectorised operations that run at C speed. For numerical work on more than a few hundred elements, NumPy is almost always faster and more memory-efficient.