Graph Representations
How to store a graph in code — adjacency lists vs adjacency matrices — and how to pick the right one for the job.
What you'll learn
- What a graph is and why it shows up everywhere (social networks, build systems, maps)
- The difference between directed and undirected graphs, and what "degree" means
- How an adjacency list stores O(V+E) space and why that matters for sparse graphs
- How an adjacency matrix gives O(1) edge lookup at the cost of O(V²) space, and when that trade is worth it
Before you start
A graph is the simplest structure that captures relationships. Two ingredients: nodes (things) and edges (connections between things). That is it. Friendships on a social network, hyperlinks between web pages, flights between airports — all graphs.
The question this lesson answers is not what a graph is but how to store one so your code can work with it efficiently.
The vocabulary you need
Directed vs undirected. A directed graph (digraph) has edges with direction — an arrow from A to B does not imply one from B to A. Twitter follows are directed. Undirected graphs treat edges as symmetric — Facebook friendships go both ways.
Weighted vs unweighted. Edges can carry a number (distance, cost, capacity). The representations below work for both; weighted versions just store a number alongside each neighbor instead of a plain 1.
Degree. For a node in an undirected graph, degree is the count of its edges. In a directed graph you distinguish in-degree (edges arriving) from out-degree (edges leaving).
Sparse vs dense. A complete graph (every pair of nodes connected) has V(V−1)/2 edges for V nodes — that is O(V²). Most real graphs are sparse: a social network of a million people does not have a trillion friendships. The choice of representation depends heavily on how sparse the graph is.
Click any edge in the diagram above. The same connection lights up in the adjacency list and the matrix simultaneously. Toggle directed/undirected to see how the matrix symmetry changes.
Adjacency list
The most common representation. Adjacency means “directly connected by an edge” — an adjacency list records, for each node, exactly which nodes it is adjacent to. Each node maps to the list of its neighbors.
graph = {
"A": ["B", "C"],
"B": ["D", "E"],
"C": ["D"],
"D": [],
"E": [],
}
Space: O(V + E). You store one entry per node (the V part — even nodes with no edges need a key in the dict) and one entry per directed edge (the E part). For an undirected graph each edge appears twice (once in each direction), so it is O(V + 2E) = O(V + E).
Iterate all neighbors of a node: O(degree) — just walk the list. This is the operation you do constantly in graph traversals (BFS, DFS, Dijkstra).
Check if edge (u, v) exists: O(degree(u)) in a plain list. If you swap the inner list for a set you get O(1), at a small overhead cost.
Add an edge: O(1) amortized (append to the list).
Adjacency matrix
A V×V 2D array. matrix[i][j] = 1 means there is an edge from node i to node j.
# A B C D E
# A [ 0, 1, 1, 0, 0 ]
# B [ 0, 0, 0, 1, 1 ]
# C [ 0, 0, 0, 1, 0 ]
# D [ 0, 0, 0, 0, 0 ]
# E [ 0, 0, 0, 0, 0 ]
matrix = [
[0, 1, 1, 0, 0], # A
[0, 0, 0, 1, 1], # B
[0, 0, 0, 1, 0], # C
[0, 0, 0, 0, 0], # D
[0, 0, 0, 0, 0], # E
]
For an undirected graph the matrix is symmetric: matrix[i][j] == matrix[j][i] always.
Space: O(V²). Always. Whether you have 6 edges or 600, you allocate V² cells. With V = 10,000 nodes that is 100 million cells — about 400 MB for 32-bit integers.
Check if edge (u, v) exists: O(1). A single array lookup. This is the matrix’s killer advantage.
Iterate all neighbors of a node: O(V). You must scan the entire row even if most entries are 0. That is painful for sparse graphs.
Picking the right one
| Question | Pick |
|---|---|
| Is the graph sparse (E much smaller than V²)? | Adjacency list |
| Do you need fast “is there an edge?” checks constantly? | Matrix |
| Are you doing BFS, DFS, or shortest-path on a large graph? | List |
| Is V small (say, under 500) and the graph dense? | Either — matrix is fine |
| Are you implementing Floyd-Warshall or similar all-pairs algorithms? | Matrix |
The rule of thumb: start with an adjacency list. Almost every graph you encounter in practice — web graphs, social graphs, package dependency graphs, road networks — is sparse. The memory penalty of O(V²) is prohibitive, and iterating neighbors is O(V) instead of O(degree).
Use a matrix when V is small, the graph is dense, and O(1) edge existence checks are the dominant operation.
Build it yourself
Run this code. It constructs an adjacency-list representation from an edge list and then prints each node’s neighbors.
Quick check
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.
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.