Graph Representations
A graph is nodes plus connections between them — no strict hierarchy required, unlike a tree. Trees, in fact, are just graphs with no cycles. Before you can run BFS/DFS (Phase 7), you need to pick how to store the graph, and that choice has real time/space consequences.
What you’ll learn
Section titled “What you’ll learn”- The three standard representations: adjacency list, adjacency matrix, edge list.
- Directed vs undirected, and weighted vs unweighted graphs.
- Time/space tradeoffs for each representation, and when to pick which.
- Building a graph from raw edge input — the step every graph problem starts with.
The cue
Section titled “The cue”The same small graph, three ways
Section titled “The same small graph, three ways”Take this undirected graph with 4 nodes and 4 edges:
graph LR
N0((0)) --- N1((1))
N0 --- N2((2))
N1 --- N2
N2 --- N3((3))
1. Adjacency list — a dict of lists
Section titled “1. Adjacency list — a dict of lists”Store, for each node, the list of nodes it connects to. This is the default choice for almost all graph algorithms — compact, and iterating a node’s neighbors is directly proportional to how many neighbors it actually has.
from collections import defaultdict
# Undirected: each edge is added BOTH ways
edges = [(0, 1), (0, 2), (1, 2), (2, 3)]
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # remove this line for a DIRECTED graph
print("adjacency list:")
for node in sorted(graph):
print(f" {node}: {graph[node]}")
print("neighbors of 2:", graph[2])2. Adjacency matrix — an n x n grid
Section titled “2. Adjacency matrix — an n x n grid”Store an n x n grid where matrix[u][v] = 1 (or a weight) if an edge
exists between u and v, else 0. Checking “are u and v connected?” is
, but the matrix costs space regardless of how many edges
actually exist.
n = 4 # number of nodes, labeled 0..3
edges = [(0, 1), (0, 2), (1, 2), (2, 3)]
matrix = [[0] * n for _ in range(n)]
for u, v in edges:
matrix[u][v] = 1
matrix[v][u] = 1 # remove this line for a DIRECTED graph
print("adjacency matrix:")
for row in matrix:
print(" ", row)
print("0 and 2 connected?", matrix[0][2] == 1)
print("0 and 3 connected?", matrix[0][3] == 1)3. Edge list — a flat list of pairs
Section titled “3. Edge list — a flat list of pairs”Just the raw list of (u, v) pairs, with no lookup structure at all. Cheapest
to build and to store, but finding a node’s neighbors means scanning every
edge — per query.
edges = [(0, 1), (0, 2), (1, 2), (2, 3)]
def neighbors_of(edges, target, directed=False):
result = []
for u, v in edges:
if u == target:
result.append(v)
elif not directed and v == target:
result.append(u)
return result
print("edge list:", edges)
print("neighbors of 2 (scan every edge):", neighbors_of(edges, 2))Edge lists show up mostly as the input format for a problem (e.g. “you
are given edges, a list of [u, v] pairs”) — you almost always convert
them into an adjacency list before running any real algorithm on them.
Directed vs undirected, weighted vs unweighted
Section titled “Directed vs undirected, weighted vs unweighted”- Undirected: an edge
(u, v)means you can travelu -> vandv -> u. Add the connection both ways (as in the adjacency list/matrix code above). - Directed: an edge
(u, v)only meansu -> v. Add the connection one way only — this is the “remove this line” comment in the code above. - Weighted: each edge carries a cost, not just a yes/no connection. In an
adjacency list, store
(neighbor, weight)pairs instead of bare neighbors; in a matrix, store the weight instead of1.
from collections import defaultdict
# Directed, weighted: edges are (source, destination, weight)
weighted_edges = [(0, 1, 4), (0, 2, 1), (2, 1, 2), (1, 3, 5)]
graph = defaultdict(list)
for u, v, w in weighted_edges:
graph[u].append((v, w)) # directed: only u -> v, carrying weight w
print("weighted, directed adjacency list:")
for node in sorted(graph):
print(f" {node}: {graph[node]}")
# cost of going straight from 0 to 2
for neighbor, weight in graph[0]:
if neighbor == 2:
print("cost 0 -> 2:", weight)Building a graph from raw edge input
Section titled “Building a graph from raw edge input”This is the very first step of nearly every graph problem: you’re handed
n (number of nodes) and edges (a list of pairs), and you build an
adjacency list before doing anything else.
from collections import defaultdict
def build_graph(n, edges, directed=False):
graph = defaultdict(list)
for node in range(n):
graph[node] # ensure every node 0..n-1 has an entry, even isolated ones
for u, v in edges:
graph[u].append(v)
if not directed:
graph[v].append(u)
return graph
n = 5
edges = [(0, 1), (1, 2), (3, 4)] # node 4 has no edges to node 0-2 -- two components
graph = build_graph(n, edges)
for node in sorted(graph):
print(f" {node}: {graph[node]}")Complexity at a glance
Section titled “Complexity at a glance”| Representation | Space | Check edge (u,v)? | Iterate neighbors of u | Add an edge |
|---|---|---|---|---|
| Adjacency list | ||||
| Adjacency matrix | ||||
| Edge list |
V = number of vertices, E = number of edges, deg(u) = degree
(neighbor count) of node u.
Where this leads: BFS/DFS in Phase 7
Section titled “Where this leads: BFS/DFS in Phase 7”Every graph traversal — BFS (shortest path in unweighted graphs), DFS (cycle detection, connected components, topological sort) — is built directly on top of the adjacency list from this lesson: “for each neighbor of the current node, do X.” Getting the representation right here is what makes those algorithms a few lines of code instead of a mess of index arithmetic.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Both of these are traversal problems — the topic right after this one once BFS/DFS are introduced.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 200Number of IslandsmediumThe grid itself *is* an implicit adjacency structure (each cell's neighbors are its 4 grid-adjacent cells); you rarely build an explicit list, but the "who are my neighbors" question is identical
- 207Course SchedulemediumBuild a directed adjacency list from prerequisite pairs, then detect a cycle (a course that depends on itself, transitively)
Visual intuition
Section titled “Visual intuition”Whatever the representation, the traversal is the same. Here BFS over an adjacency list, with the frontier structure on screen:
Nodes are marked seen on ENQUEUE, not on dequeue. Marking on dequeue lets a node reachable by two edges enter the queue twice — the standard way this degrades.
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”Each exercise is the actual LeetCode problem with its real method signature and LeetCode’s own examples as the test. Write the body, press Run, and match the expected output.
LC 1791 — Find Center of Star Graph · Easy
Section titled “LC 1791 — Find Center of Star Graph · Easy”Problem. A star graph has one centre connected to every other node. Given its edge list, return the centre.
Constraints. 3 <= n <= 10^5, exactly n - 1 edges, and the input is
guaranteed to be a valid star.
Examples. [[1,2],[2,3],[4,2]] gives 2 ·
[[1,2],[5,1],[1,3],[1,4]] gives 1
Editorial
Since every edge touches the centre, the shared endpoint of any two edges is the centre.
Time . Space .
The obvious solution counts degrees and returns the node with degree n - 1. That is
and perfectly correct — worth stating — but the structure lets you stop after
two edges. Recognising that the guarantee (“it is a valid star”) licenses the
shortcut is the point of the problem.
Follow-ups: “Verify it really is a star?” — then you do need the degree count:
one node with degree n - 1 and the rest with degree 1. “Find the centre of a
tree (LC 310)?” — much harder: peel leaves layer by layer until one or two
nodes remain. “What if the graph were given as an adjacency list?” — the node whose
list has length n - 1.
LC 997 — Find the Town Judge · Easy
Section titled “LC 997 — Find the Town Judge · Easy”Problem. In a town of n people, the judge trusts nobody, everybody else
trusts the judge, and there is at most one judge. Given trust pairs
[a, b] meaning a trusts b, return the judge’s label or -1.
Constraints. 1 <= n <= 1000, 0 <= len(trust) <= 10^4, pairs are distinct.
Examples. n = 2, trust = [[1,2]] gives 2 ·
n = 3, trust = [[1,3],[2,3]] gives 3 ·
n = 3, trust = [[1,3],[2,3],[3,1]] gives -1
Editorial
The judge needs in-degree n - 1 and out-degree 0. Combining them into a single
net score is what makes the solution three lines: only a person with in-degree
n - 1 and out-degree 0 can reach n - 1, since any outgoing trust subtracts.
Time . Space .
Two cases worth checking:
n = 1, trust = []gives1. The lone person trusts nobody and is trusted by nobody, andn - 1 == 0matches their score. A solution that requires at least one trust edge misses this.[[1,3],[2,3],[3,1]]gives-1. Person 3 is trusted by two others but also trusts person 1, so their score is2 - 1 = 1, not2.
This is a degree-counting problem, which is the cheapest form of graph analysis — no adjacency structure or traversal is needed at all. Recognising when in/out-degrees alone answer the question saves building a graph you never traverse.
Follow-ups: “Two separate arrays for in and out degree?” — equally correct, and
arguably clearer; the single score is just tidier. “Find a celebrity in
queries (LC 277)?” — a related problem solved by eliminating candidates pairwise.
“What if several judges were allowed?” — collect every person scoring n - 1.
LC 133 — Clone Graph · Medium
Section titled “LC 133 — Clone Graph · Medium”Problem. Given a reference to a node in a connected undirected graph, return
a deep copy. Each node has a val and a list of neighbors.
Constraints. 0 <= number of nodes <= 100, values are unique, the graph has no
self-loops or repeated edges and is connected.
Examples. The square 1-2-3-4-1 must be copied so that the structure matches and
no node object is shared with the original.
Editorial
A single map from original node to its copy does two jobs: it is the copy registry, and it is the visited set. That is why the traversal terminates on a cyclic graph.
Time . Space .
Registering before recursing is essential. In the square graph, node 1’s
neighbour 2 has node 1 as its own neighbour. If clone[n] = copy came after the
neighbour loop, the recursion would bounce between them until the stack overflowed.
Inserting first means the second visit finds the entry and returns immediately.
This is the graph version of LC 138 Copy List with Random Pointer. There the linear order let you create all nodes in one pass and wire them in a second; here there is no order, so creation and wiring interleave — and the map is what keeps that consistent.
BFS works identically: create the copy on first sight, enqueue, and wire neighbours as you dequeue.
Follow-ups: “Iteratively?” — BFS with a queue and the same map. “Disconnected
graph?” — the problem guarantees connectivity from the given node; otherwise you
would need to iterate over all nodes. “Directed graph?” — the same code works
unchanged. “Why not copy.deepcopy?” — it works but defeats the exercise.
Dry run
Section titled “Dry run”The same graph — edges A-B, A-C, B-D, C-D — in all three forms.
Adjacency list (undirected, so each edge appears twice):
A: [B, C] B: [A, D] C: [A, D] D: [B, C]Adjacency matrix:
| A | B | C | D | |
|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 |
| B | 1 | 0 | 0 | 1 |
| C | 1 | 0 | 0 | 1 |
| D | 0 | 1 | 1 | 0 |
Edge list: [(A,B), (A,C), (B,D), (C,D)]
Now the cost comparison for four nodes and four edges:
| operation | list | matrix | edge list |
|---|---|---|---|
| storage | 8 entries | 16 cells | 4 tuples |
neighbours of A | = 2 | = 4 | = 4 |
is there an A-D edge? | |||
| iterate all edges |
At with , the matrix costs a million cells to store two thousand edges — and every neighbour lookup scans a thousand entries to find two. That is the concrete reason the list is the default.
The variant map
Section titled “The variant map”| Situation | Representation | Why |
|---|---|---|
| Sparse, traversal-heavy | adjacency list | space, cheap neighbour iteration |
| Dense, or repeated edge tests | adjacency matrix | edge lookup |
| Sort edges by weight (Kruskal) | edge list | the algorithm only ever iterates edges |
| All-pairs shortest paths (Floyd-Warshall) | adjacency matrix | the algorithm is matrix updates |
| Grid or maze | implicit — compute neighbours from (r, c) | building a graph object is wasted work |
| Weighted | store (neighbour, weight) tuples in the list | keeps Dijkstra straightforward |
| Nodes are strings or objects | defaultdict(list) keyed by the object | avoids an id-mapping layer |
Pitfalls
Section titled “Pitfalls”- Adding only one direction for an undirected edge. Half the graph becomes unreachable, and the symptom looks like a traversal bug.
- Using a matrix when the graph is sparse. memory for edges, and neighbour iteration slows to .
- Forgetting isolated nodes. Building the graph purely from an edge list drops any node with no edges — which changes the component count.
- Not handling self-loops or duplicate edges. Both are legal in many problems and both break naive assumptions.
- Building a graph object for a grid. Compute neighbours from
(r, c)instead; the graph is implicit. - Using
dictwhere node ids are0..n-1. A list of lists is faster and simpler for a dense integer range.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “List or matrix here?” | Judgement | Sparse and traversal-heavy means list (). Dense or repeated edge tests means matrix ( lookup, space) |
| “Space complexity of each?” | Precision | List ; matrix ; edge list |
| “How do you represent a grid as a graph?” | Whether you see implicit graphs | You do not build one — derive neighbours from (r, c) with a DIRS list. The graph is implicit |
| “Weighted edges?” | Practical detail | Store (neighbour, weight) tuples in the list, or put the weight in the matrix cell |
| “Node ids are arbitrary strings” | Modelling | defaultdict(list) keyed directly by the string, or map to integers first if the algorithm needs indices |
| “When is an edge list the right choice?” | Breadth | When the algorithm only iterates or sorts edges — Kruskal’s MST is the canonical case |
Self-check
Section titled “Self-check”-
Space complexity of an adjacency list versus an adjacency matrix?
At V = 1000 with E = 2000, the matrix stores a million cells for two thousand edges. That ratio is the whole argument.
pch.quizShowAnswer
B — List O(V + E); matrix O(V squared) — which is why the list is the default for sparse graphs — At V = 1000 with E = 2000, the matrix stores a million cells for two thousand edges. That ratio is the whole argument.
-
When is an adjacency matrix the better choice?
The O(1) edge test is the matrix's only real advantage, and it only pays for itself when the graph is dense or you use it often.
pch.quizShowAnswer
B — When the graph is dense, or you repeatedly test specific edges, or the algorithm is matrix-shaped like Floyd-Warshall — The O(1) edge test is the matrix's only real advantage, and it only pays for itself when the graph is dense or you use it often.
-
You are building an undirected graph from an edge list. What is the classic bug?
The symptom looks like a traversal bug rather than a setup bug, which is what makes it expensive. Both adj[u].append(v) and adj[v].append(u) are needed.
pch.quizShowAnswer
B — Adding only one direction, so half the graph becomes unreachable — The symptom looks like a traversal bug rather than a setup bug, which is what makes it expensive. Both adj[u].append(v) and adj[v].append(u) are needed.
-
How should a grid maze be represented as a graph?
Materialising the graph is wasted work and memory. Recognising a graph problem that has no graph object in it is the more valuable skill.
pch.quizShowAnswer
B — Do not build one — derive neighbours from (r, c) using a DIRS list. The graph is implicit — Materialising the graph is wasted work and memory. Recognising a graph problem that has no graph object in it is the more valuable skill.
Recall card
Section titled “Recall card”- Adjacency list —
defaultdict(list). space. The default. - Adjacency matrix — space, edge test. Use when dense, when edge tests repeat, or for Floyd-Warshall.
- Edge list — space, no traversal. Use when sorting edges (Kruskal).
- Implicit — grids and state spaces build nothing; neighbours are derived. This is the most common interview shape.
- Undirected means adding both directions. Forgetting the reverse edge is the classic setup bug.
- Rule of thumb — in the hundreds and dense → matrix. Otherwise list.
- Adjacency list ( space): the default, best for iterating a node’s neighbors — what nearly every graph algorithm expects.
- Adjacency matrix ( space): edge checks, worth it only for dense graphs or frequent direct-connection queries.
- Edge list ( space): the common input format, converted to an adjacency list before real work starts.
- Undirected = add both directions; directed = add one; weighted = carry a value alongside each neighbor.
- Always pre-seed nodes
0..n-1so isolated nodes aren’t silently dropped.
Next up in Phase 7: BFS and DFS run directly on the adjacency list built here — shortest paths, connected components, and cycle detection.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading