Graph representation
A graph is a set of vertices joined by edges. You can store it three ways. An adjacency list gives each vertex its list of neighbors: O(V + E) memory and the fastest neighbor iteration, so it’s the default. An adjacency matrix is a V×V grid: O(1) edge checks but O(V²) memory, only sensible for small or dense graphs. An edge list is just the pairs: compact, and the natural input for sorting edges (Kruskal) or union-find.
The idea in plain words
Section titled “The idea in plain words”Think of a city’s airports and flights. You could describe them three ways:
- A departures board per airport (adjacency list): “From SFO you can fly to JFK, SEA, LAX.” To see where you can go next, read one board.
- A giant mileage chart (adjacency matrix): rows and columns are airports, and each cell says whether there’s a flight. Instant lookup for any pair, but the chart has a cell for every pair of airports in the world, flights or not.
- The airline’s raw flight log (edge list): “SFO→JFK, JFK→LHR, …”. Easy to write down, easy to sort, slow to ask anything specific.
They hold identical information. They differ in what’s cheap to ask. Graph algorithms mostly ask one thing over and over: “what are the neighbors of v?” That’s why the adjacency list wins most of the time.
Vocabulary you’ll need:
| Term | Meaning |
|---|---|
| directed / undirected | edges have a direction (follows, depends-on) or not (friendship, roads both ways) |
| weighted | each edge carries a number (distance, cost, capacity) |
| degree | number of edges at a vertex; directed graphs split it into in-degree and out-degree |
| sparse / dense | E much closer to V than to V², or the reverse. Most real graphs are sparse |
| path, cycle | a walk along edges; a cycle returns to its start |
| connected component | a maximal group of vertices that can all reach each other |
| DAG | directed acyclic graph: directed, no cycles (dependencies, schedules) |
| self-loop, multi-edge | an edge from v to itself; two edges between the same pair. Ask whether they’re allowed |
See it
Section titled “See it”Click two vertices to add an edge (click an edge to remove it), or click matrix cells. All three representations update together. Then pick a query and see what each representation has to read to answer it.
Things to try:
- Make it directed. The matrix stops being symmetric, and each edge appears in only one list row.
- Choose list neighbors of u and press complete. The list row and the matrix row now cost the same: dense graphs are where matrices stop losing.
- Choose visit every edge with only a few edges. The matrix still reads every cell.
When to reach for it
Section titled “When to reach for it”| The problem says… | Representation | Why |
|---|---|---|
| “given a list of edges / pairs / dependencies”, then BFS, DFS, shortest paths | adjacency list | You’ll iterate neighbors V times; total O(V + E). |
| vertices are strings or objects (users, airports, packages) | hash map → list or set | No need to number vertices 0..n−1 first. |
| “is there an edge between u and v?” asked constantly, V ≤ a few thousand | adjacency matrix (or list of hash sets) | O(1) per check. |
| all-pairs shortest paths (Floyd–Warshall), dense graphs, V ≤ ~500 | matrix | The algorithm is O(V³) over the matrix anyway. |
| “sort edges by weight”, “process edges in order”, union-find | edge list | You only ever scan or sort the pairs. |
| a grid, a puzzle, word transformations | implicit graph | Don’t build anything: compute neighbors on demand. See grids and state-space search. |
How it works
Section titled “How it works”- Make a row for every vertex, even ones with no edges. Isolated vertices are real vertices; forgetting them is a classic bug.
- For each edge u→v, append v to u’s row. If the graph is undirected, also append u to v’s row. Every undirected edge is stored twice.
- Matrix: set
M[u][v](andM[v][u]if undirected). Weighted graphs store the weight instead oftrue, with ∞ ornullfor “no edge”. - Named vertices: use a hash map from name to neighbors instead of an array, and a set for neighbors if you’ll check edges or need to ignore duplicates.
from collections import defaultdict
def to_adj_list(n: int, edges: list[tuple[int, int]], directed: bool) -> list[list[int]]: adj: list[list[int]] = [[] for _ in range(n)] # 1. one (possibly empty) row per vertex for u, v in edges: adj[u].append(v) # 2. record u -> v if not directed: adj[v].append(u) # undirected: store both directions return adj
def to_matrix(n: int, edges: list[tuple[int, int]], directed: bool) -> list[list[bool]]: m = [[False] * n for _ in range(n)] # V*V cells, even with zero edges for u, v in edges: m[u][v] = True # 3. O(1) "is there an edge?" later if not directed: m[v][u] = True return m
def to_adj_map(edges: list[tuple[str, str]]) -> dict[str, set[str]]: """Vertices are names, not 0..n-1: a dict of sets. Sets also make edge checks O(1).""" adj: defaultdict[str, set[str]] = defaultdict(set) for u, v in edges: adj[u].add(v) adj[v].add(u) return dict(adj)
if __name__ == "__main__": edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)] assert to_adj_list(5, edges, directed=False) == [[1, 2], [0, 3], [0, 3], [1, 2, 4], [3]] assert to_adj_list(5, edges, directed=True) == [[1, 2], [3], [3], [4], []] m = to_matrix(5, edges, directed=False) assert m[3][1] and m[1][3] and not m[0][4] assert to_adj_list(3, [], directed=True) == [[], [], []] # isolated vertices still exist g = to_adj_map([("sfo", "jfk"), ("jfk", "lhr")]) assert g["jfk"] == {"sfo", "lhr"} print("ok")import assert from "node:assert/strict";
export function toAdjList(n: number, edges: [number, number][], directed: boolean): number[][] { const adj: number[][] = Array.from({ length: n }, () => []); // 1. one (possibly empty) row per vertex for (const [u, v] of edges) { adj[u].push(v); // 2. record u -> v if (!directed) adj[v].push(u); // undirected: store both directions } return adj;}
export function toMatrix(n: number, edges: [number, number][], directed: boolean): boolean[][] { // NOT new Array(n).fill(new Array(n)): that shares one row object across every row. const m = Array.from({ length: n }, () => new Array<boolean>(n).fill(false)); // V*V cells for (const [u, v] of edges) { m[u][v] = true; // 3. O(1) "is there an edge?" later if (!directed) m[v][u] = true; } return m;}
/** Vertices are names, not 0..n-1: a Map of Sets. Sets also make edge checks O(1). */export function toAdjMap(edges: [string, string][]): Map<string, Set<string>> { const adj = new Map<string, Set<string>>(); const add = (a: string, b: string) => { if (!adj.has(a)) adj.set(a, new Set()); adj.get(a)!.add(b); }; for (const [u, v] of edges) { add(u, v); add(v, u); } return adj;}
const edges: [number, number][] = [[0, 1], [0, 2], [1, 3], [2, 3], [3, 4]];assert.deepEqual(toAdjList(5, edges, false), [[1, 2], [0, 3], [0, 3], [1, 2, 4], [3]]);assert.deepEqual(toAdjList(5, edges, true), [[1, 2], [3], [3], [4], []]);const m = toMatrix(5, edges, false);assert.ok(m[3][1] && m[1][3] && !m[0][4]);assert.deepEqual(toAdjList(3, [], true), [[], [], []]); // isolated vertices still existassert.deepEqual([...toAdjMap([["sfo", "jfk"], ["jfk", "lhr"]]).get("jfk")!], ["sfo", "lhr"]);console.log("ok");(int, int)[] edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4)];var und = ToAdjList(5, edges, directed: false);if (string.Join("|", und.Select(r => string.Join(",", r))) != "1,2|0,3|0,3|1,2,4|3") throw new Exception("undirected");var dir = ToAdjList(5, edges, directed: true);if (dir[4].Count != 0 || dir[3].Single() != 4) throw new Exception("directed");var m = ToMatrix(5, edges, directed: false);if (!m[3, 1] || !m[1, 3] || m[0, 4]) throw new Exception("matrix");var g = ToAdjMap([("sfo", "jfk"), ("jfk", "lhr")]);if (!g["jfk"].SetEquals(["sfo", "lhr"])) throw new Exception("named vertices");Console.WriteLine("ok");
static List<int>[] ToAdjList(int n, (int U, int V)[] edges, bool directed){ var adj = new List<int>[n]; for (int i = 0; i < n; i++) adj[i] = []; // 1. one (possibly empty) row per vertex foreach (var (u, v) in edges) { adj[u].Add(v); // 2. record u -> v if (!directed) adj[v].Add(u); // undirected: store both directions } return adj;}
static bool[,] ToMatrix(int n, (int U, int V)[] edges, bool directed){ var m = new bool[n, n]; // V*V cells, even with zero edges foreach (var (u, v) in edges) { m[u, v] = true; // 3. O(1) "is there an edge?" later if (!directed) m[v, u] = true; } return m;}
// Vertices are names, not 0..n-1: a dictionary of sets. Sets also make edge checks O(1).static Dictionary<string, HashSet<string>> ToAdjMap((string U, string V)[] edges){ var adj = new Dictionary<string, HashSet<string>>(); foreach (var (u, v) in edges) { if (!adj.TryGetValue(u, out var su)) adj[u] = su = []; if (!adj.TryGetValue(v, out var sv)) adj[v] = sv = []; su.Add(v); sv.Add(u); } return adj;}Two operations come up constantly as building blocks: reversing a directed graph (who points at me?) and counting degrees. Both are one pass over every edge.
def transpose(adj: list[list[int]]) -> list[list[int]]: """Reverse every edge of a directed graph: u -> v becomes v -> u. O(V + E).""" rev: list[list[int]] = [[] for _ in adj] for u, outs in enumerate(adj): for v in outs: rev[v].append(u) return rev
def degrees(adj: list[list[int]]) -> tuple[list[int], list[int]]: out_deg = [len(outs) for outs in adj] # out-degree: free, it's the row length in_deg = [0] * len(adj) for outs in adj: # in-degree: one pass over every edge for v in outs: in_deg[v] += 1 return in_deg, out_deg
if __name__ == "__main__": adj = [[1, 2], [3], [3], [4], []] # 0->1, 0->2, 1->3, 2->3, 3->4 assert transpose(adj) == [[], [0], [0], [1, 2], [3]] assert transpose(transpose(adj)) == adj # reversing twice gives the original back assert degrees(adj) == ([0, 1, 1, 2, 1], [2, 1, 1, 1, 0]) print("ok")import assert from "node:assert/strict";
/** Reverse every edge of a directed graph: u -> v becomes v -> u. O(V + E). */export function transpose(adj: number[][]): number[][] { const rev: number[][] = adj.map(() => []); adj.forEach((outs, u) => { for (const v of outs) rev[v].push(u); }); return rev;}
export function degrees(adj: number[][]): { inDeg: number[]; outDeg: number[] } { const outDeg = adj.map((outs) => outs.length); // out-degree: free, it's the row length const inDeg = new Array<number>(adj.length).fill(0); for (const outs of adj) for (const v of outs) inDeg[v]++; // in-degree: one pass over every edge return { inDeg, outDeg };}
const adj = [[1, 2], [3], [3], [4], []]; // 0->1, 0->2, 1->3, 2->3, 3->4assert.deepEqual(transpose(adj), [[], [0], [0], [1, 2], [3]]);assert.deepEqual(transpose(transpose(adj)), adj); // reversing twice gives the original backassert.deepEqual(degrees(adj), { inDeg: [0, 1, 1, 2, 1], outDeg: [2, 1, 1, 1, 0] });console.log("ok");List<int>[] adj = [[1, 2], [3], [3], [4], []]; // 0->1, 0->2, 1->3, 2->3, 3->4var rev = Transpose(adj);if (string.Join("|", rev.Select(r => string.Join(",", r))) != "|0|0|1,2|3") throw new Exception("transpose");var back = Transpose(rev);if (!back.Zip(adj).All(p => p.First.SequenceEqual(p.Second))) throw new Exception("twice = original");var (inDeg, outDeg) = Degrees(adj);if (!inDeg.SequenceEqual([0, 1, 1, 2, 1]) || !outDeg.SequenceEqual([2, 1, 1, 1, 0])) throw new Exception("degrees");Console.WriteLine("ok");
// Reverse every edge of a directed graph: u -> v becomes v -> u. O(V + E).static List<int>[] Transpose(List<int>[] adj){ var rev = adj.Select(_ => new List<int>()).ToArray(); for (int u = 0; u < adj.Length; u++) foreach (var v in adj[u]) rev[v].Add(u); return rev;}
static (int[] In, int[] Out) Degrees(List<int>[] adj){ var outDeg = adj.Select(r => r.Count).ToArray(); // out-degree: free, it's the row length var inDeg = new int[adj.Length]; foreach (var outs in adj) // in-degree: one pass over every edge foreach (var v in outs) inDeg[v]++; return (inDeg, outDeg);}What it costs
Section titled “What it costs”| Adjacency list | Adjacency matrix | Edge list | |
|---|---|---|---|
| memory | O(V + E) | O(V²) | O(E) |
| add edge | O(1) | O(1) | O(1) |
| remove edge | O(deg u) (O(1) with sets) | O(1) | O(E) |
| is u–v an edge? | O(deg u) (O(1) with sets) | O(1) | O(E) |
| iterate neighbors of u | O(deg u) | O(V) | O(E) |
| iterate all edges | O(V + E) | O(V²) | O(E) |
| BFS / DFS total | O(V + E) | O(V²) | O(V·E) unless converted |
The matrix’s weakness is memory, and it grows faster than intuition suggests:
Variations you’ll meet
Section titled “Variations you’ll meet”- Adjacency sets (
dict[node, set[node]]): O(1) edge checks and removals with list-like memory. The best general-purpose interview choice when edges can be removed or duplicated. - Both directions for directed graphs. Keep
out[u]andin[u]when you need predecessors as often as successors. - Implicit graphs. Neighbors come from a rule: the 4 or 8 cells around a grid square, the moves of a knight, every string one edit away. Write a
neighbors(state)function instead of building edges up front. - Compressed sparse row (CSR). Two flat arrays:
offsets[V+1]andtargets[E]. Neighbors of u aretargets[offsets[u]:offsets[u+1]]. Cache-friendly; what graph libraries use internally. - Incidence structures, multigraphs, hypergraphs. Rare in interviews; mention only if asked.
- Trees are graphs too: connected, acyclic, exactly V − 1 edges. A parent array is a tree representation.
Pitfalls
Section titled “Pitfalls”- Adding undirected edges once. Then BFS from one end can’t reach back. Append both directions.
- Dropping isolated vertices by building the list only from edges. Size the structure from n (or the vertex list), not from the edges.
- Shared rows in JS:
new Array(n).fill([])puts the same array in every slot. UseArray.from({ length: n }, () => []). Python’s[[]] * nhas the same bug. - Duplicate edges in a list representation double-count degrees and neighbors. Use sets if the input can repeat edges.
- Self-loops count twice toward degree in undirected graphs by convention. Decide whether to allow them.
- Building a matrix for large V. 10⁵ vertices is 10¹⁰ cells. Ask about V before reaching for one.
- Recursion depth when the representation feeds DFS on a long path graph. See DFS.
In the interview
Section titled “In the interview”- First ask: directed or undirected? weighted? how many vertices and edges? can there be self-loops or duplicate edges? are vertices numbered or named?
- Say the choice and why: “Adjacency list as a dict of sets: O(V + E) memory, O(1) edge checks, and the vertices are strings.”
- Give complexities in V and E, not n.
- Expect follow-ups: “What if the graph doesn’t fit in memory?” (edge list on disk, sort by source, stream; or partition by vertex), “What if edges are added and removed constantly?” (sets), “What if you need predecessors too?” (keep the reverse graph).
Check yourself
Section titled “Check yourself”Practice on your own
Section titled “Practice on your own”These are classic textbook exercises, not gym questions:
- Convert an adjacency matrix to an adjacency list and back; check you get the original.
- Find the “town judge”: the person everyone trusts who trusts nobody (in-degree n−1, out-degree 0).
- Find the center of a star graph from its edge list.
- Clone a connected undirected graph given one node, using a map from old node to new node.
- Is there a path from
stot? Solve it once with BFS on an adjacency list and once with union-find on the edge list.
Go deeper
Section titled “Go deeper”- CLRS, ch. 20 (ch. 22 in older editions) “Elementary Graph Algorithms”, §1 “Representations of graphs”.
- Sedgewick and Wayne, Algorithms §4.1 Undirected Graphs and §4.2 Directed Graphs: the bag-of-neighbors API.
- Skiena, The Algorithm Design Manual, §7.2 “Data Structures for Graphs”: a practical comparison with sizes.
- cp-algorithms: graph theory section, for the algorithms you’ll build on top of these representations.