Chapter 32: Graph Theory Basics and Representations
32.03Intermediate / Advanced5 topics17 min4 interactive animations

Representations

The ways to store a graph: adjacency matrix, adjacency list, edge list, incidence matrix and compressed sparse row, with what each costs and does well.

Learning goals

  • Build an adjacency matrix, an adjacency list and an edge list from the same edges
  • Read neighbours and test for an edge in each representation
  • Describe the incidence matrix and its column sums
  • Build a compressed sparse row (CSR) graph in two passes

Prerequisites

ArraysListsGraph vocabulary

The same road map can be a table of distances between every pair of towns, a list of neighbours for each town, or just a list of roads. Each is the right tool for some questions and a poor one for others.

32.03.1

Adjacency matrix

The **adjacency matrix** is an n × n table with matrix[a][b] set when there is an edge from a to b (a weight for weighted graphs, a count for multigraphs). Asking whether an edge exists is one array read, O(1). The price is n² cells whatever the number of edges, and listing the neighbours of one vertex means scanning a whole row, O(n).

Watch the matrix in the animation as edges arrive. An undirected edge fills two mirror cells, a directed edge one. A self-loop adds 2 on the diagonal in this course, so every row sum equals the degree; other books put 1. The preset "Duplicate edge and self-loop" shows a cell counting 2 for a repeated edge.

Java
1class AdjMatrix {
2 private final int[][] m;
3 private final boolean directed;
4 AdjMatrix(int n, boolean directed) { m = new int[n][n]; this.directed = directed; }
5 void addEdge(int a, int b) {
6 m[a][b]++;
7 if (!directed) m[b][a]++; // a self-loop counts twice on the diagonal
8 }
9 boolean hasEdge(int a, int b) { return m[a][b] > 0; } // O(1)
10 List<Integer> neighbours(int v) { // O(n): scan the whole row
11 List<Integer> out = new ArrayList<>();
12 for (int u = 0; u < m.length; u++) if (m[v][u] > 0) out.add(u);
13 return out;
14 }
15}

One graph, three representations: matrix, adjacency list, edge list

Interactive · edit the input

Your input

Try

grid · 6 × 6

012345
0
0
1
2
3
4
1
0
0
0
0
0
0
2
1
0
0
0
0
0
3
2
0
0
0
0
0
4
3
0
0
0
0
0
5
4
0
0
0
0
0
Just addedChanged entryHeader

undirected graph · 5 vertices · 0 edges

0
1
2
3
4
Just addedChanged entryHeader

list[0]

empty

list[1]

empty

list[2]

empty

list[3]

empty

list[4]

empty

edge list (a-b, weight)

empty
Setup

An empty undirected graph on 5 vertices. The three representations below are always updated together: the matrix always has 5 × 5 = 25 cells, the adjacency list grows with the edges, and the edge list is just the edges.

Step 1 / 7
Java
1int[][] matrix = new int[n][n]; // adjacency matrix: n * n cells
2List<List<int[]>> list = new ArrayList<>(); // adjacency list: {neighbour, weight} per vertex
3for (int i = 0; i < n; i++) list.add(new ArrayList<>());
4int[][] edgeList = new int[m][]; // edge list: {a, b, weight}
5void addEdge(int k, int a, int b, int w, boolean directed) {
6 edgeList[k] = new int[]{a, b, w};
7 matrix[a][b]++; // directed: this one cell is the whole edge
8 list.get(a).add(new int[]{b, w});
9 if (!directed) {
10 matrix[b][a]++; // undirected: also the mirror cell (a self-loop counts twice on the diagonal)
11 list.get(b).add(new int[]{a, w});
12 }
13}

Live state

0 edges added0 list entries

n

5

edges

5

No condition evaluated on this step
Execution trace (dry run table)Show 7 steps
StepLineStateWhat happens
1L1n=5 edges=5An empty undirected graph on 5 vertices. The three representations below are always updated together: the matrix always has 5 × 5 = 25 cells, the adjacency list grows with the edges, and the edge list is just the edges.
2L10edge=0-1 matrix cells changed=2 list entries=2Add the edge 0-1: both mirror cells [0][1] and [1][0], two list entries and one edge-list entry.
3L10edge=0-2 matrix cells changed=2 list entries=4Add the edge 0-2: both mirror cells [0][2] and [2][0], two list entries and one edge-list entry.
4L10edge=1-3 matrix cells changed=2 list entries=6Add the edge 1-3: both mirror cells [1][3] and [3][1], two list entries and one edge-list entry.
5L10edge=2-3 matrix cells changed=2 list entries=8Add the edge 2-3: both mirror cells [2][3] and [3][2], two list entries and one edge-list entry.
6L10edge=3-4 matrix cells changed=2 list entries=10Add the edge 3-4: both mirror cells [3][4] and [4][3], two list entries and one edge-list entry.
7L13return=[2, 2, 2, 3, 1] sum of degrees=10 edges=5Degrees (row sums) [2, 2, 2, 3, 1]; they add up to 10 = 2 × 5: every edge has two ends (handshaking lemma).
32.03.2

Adjacency list

The **adjacency list** keeps, for every vertex, the list of its neighbours. It uses n + m space (n + 2m entries for an undirected graph), listing the neighbours of a vertex takes time proportional to its degree, and a whole traversal takes O(V + E). Testing whether one particular edge exists means scanning the list, O(degree). It is the default representation for most graph algorithms.

In the animation each vertex has a row list[v] that grows as edges are added; an undirected edge appears in both endpoints' lists, a directed edge only in the tail's. For weighted graphs each entry carries the weight (neighbour:weight).

Java
1class AdjList {
2 private final List<List<int[]>> g = new ArrayList<>(); // {neighbour, weight}
3 private final boolean directed;
4 AdjList(int n, boolean directed) {
5 this.directed = directed;
6 for (int i = 0; i < n; i++) g.add(new ArrayList<>());
7 }
8 void addEdge(int a, int b, int w) {
9 g.get(a).add(new int[]{b, w});
10 if (!directed) g.get(b).add(new int[]{a, w});
11 }
12 List<int[]> neighbours(int v) { return g.get(v); } // O(degree)
13 int degree(int v) { return g.get(v).size(); }
14 boolean hasEdge(int a, int b) { // O(degree of a)
15 for (int[] e : g.get(a)) if (e[0] == b) return true;
16 return false;
17 }
18}

One graph, three representations: matrix, adjacency list, edge list

Interactive · edit the input

Your input

Try

grid · 6 × 6

012345
0
0
1
2
3
4
1
0
0
0
0
0
0
2
1
0
0
0
0
0
3
2
0
0
0
0
0
4
3
0
0
0
0
0
5
4
0
0
0
0
0
Just addedChanged entryHeader

undirected graph · 5 vertices · 0 edges

0
1
2
3
4
Just addedChanged entryHeader

list[0]

empty

list[1]

empty

list[2]

empty

list[3]

empty

list[4]

empty

edge list (a-b, weight)

empty
Setup

An empty undirected graph on 5 vertices. The three representations below are always updated together: the matrix always has 5 × 5 = 25 cells, the adjacency list grows with the edges, and the edge list is just the edges.

Step 1 / 7
Java
1int[][] matrix = new int[n][n]; // adjacency matrix: n * n cells
2List<List<int[]>> list = new ArrayList<>(); // adjacency list: {neighbour, weight} per vertex
3for (int i = 0; i < n; i++) list.add(new ArrayList<>());
4int[][] edgeList = new int[m][]; // edge list: {a, b, weight}
5void addEdge(int k, int a, int b, int w, boolean directed) {
6 edgeList[k] = new int[]{a, b, w};
7 matrix[a][b]++; // directed: this one cell is the whole edge
8 list.get(a).add(new int[]{b, w});
9 if (!directed) {
10 matrix[b][a]++; // undirected: also the mirror cell (a self-loop counts twice on the diagonal)
11 list.get(b).add(new int[]{a, w});
12 }
13}

Live state

0 edges added0 list entries

n

5

edges

5

No condition evaluated on this step
Execution trace (dry run table)Show 7 steps
StepLineStateWhat happens
1L1n=5 edges=5An empty undirected graph on 5 vertices. The three representations below are always updated together: the matrix always has 5 × 5 = 25 cells, the adjacency list grows with the edges, and the edge list is just the edges.
2L10edge=0-1 matrix cells changed=2 list entries=2Add the edge 0-1: both mirror cells [0][1] and [1][0], two list entries and one edge-list entry.
3L10edge=0-2 matrix cells changed=2 list entries=4Add the edge 0-2: both mirror cells [0][2] and [2][0], two list entries and one edge-list entry.
4L10edge=1-3 matrix cells changed=2 list entries=6Add the edge 1-3: both mirror cells [1][3] and [3][1], two list entries and one edge-list entry.
5L10edge=2-3 matrix cells changed=2 list entries=8Add the edge 2-3: both mirror cells [2][3] and [3][2], two list entries and one edge-list entry.
6L10edge=3-4 matrix cells changed=2 list entries=10Add the edge 3-4: both mirror cells [3][4] and [4][3], two list entries and one edge-list entry.
7L13return=[2, 2, 2, 3, 1] sum of degrees=10 edges=5Degrees (row sums) [2, 2, 2, 3, 1]; they add up to 10 = 2 × 5: every edge has two ends (handshaking lemma).
32.03.3

Edge list

The **edge list** is just the list of edges, each as {a, b, weight}. It is the most compact form (m entries) and the natural input format. It is exactly what algorithms that process edges in an order want: Kruskal's algorithm sorts it by weight, Bellman-Ford loops over it. It is the worst for neighbour queries, which need a scan of the whole list, O(m).

Keep the original edge list even when you build an adjacency list for searching: it costs little and many algorithms (and reports, and tests) need the edges in input order. The animation shows it as the last row, one entry per edge in the order they were added.

Java
1class EdgeList {
2 final List<int[]> edges = new ArrayList<>(); // {a, b, weight}
3 void add(int a, int b, int w) { edges.add(new int[]{a, b, w}); }
4 int[] degrees(int n) { // O(m): one pass over the edges
5 int[] deg = new int[n];
6 for (int[] e : edges) { deg[e[0]]++; deg[e[1]]++; }
7 return deg;
8 }
9 List<int[]> sortedByWeight() { // what Kruskal's algorithm starts with
10 List<int[]> copy = new ArrayList<>(edges);
11 copy.sort(Comparator.comparingInt(e -> e[2]));
12 return copy;
13 }
14}

One graph, three representations: matrix, adjacency list, edge list

Interactive · edit the input

Your input

Try

grid · 6 × 6

012345
0
0
1
2
3
4
1
0
0
0
0
0
0
2
1
0
0
0
0
0
3
2
0
0
0
0
0
4
3
0
0
0
0
0
5
4
0
0
0
0
0
Just addedChanged entryHeader

undirected graph · 5 vertices · 0 edges

0
1
2
3
4
Just addedChanged entryHeader

list[0]

empty

list[1]

empty

list[2]

empty

list[3]

empty

list[4]

empty

edge list (a-b, weight)

empty
Setup

An empty undirected graph on 5 vertices. The three representations below are always updated together: the matrix always has 5 × 5 = 25 cells, the adjacency list grows with the edges, and the edge list is just the edges.

Step 1 / 7
Java
1int[][] matrix = new int[n][n]; // adjacency matrix: n * n cells
2List<List<int[]>> list = new ArrayList<>(); // adjacency list: {neighbour, weight} per vertex
3for (int i = 0; i < n; i++) list.add(new ArrayList<>());
4int[][] edgeList = new int[m][]; // edge list: {a, b, weight}
5void addEdge(int k, int a, int b, int w, boolean directed) {
6 edgeList[k] = new int[]{a, b, w};
7 matrix[a][b]++; // directed: this one cell is the whole edge
8 list.get(a).add(new int[]{b, w});
9 if (!directed) {
10 matrix[b][a]++; // undirected: also the mirror cell (a self-loop counts twice on the diagonal)
11 list.get(b).add(new int[]{a, w});
12 }
13}

Live state

0 edges added0 list entries

n

5

edges

5

No condition evaluated on this step
Execution trace (dry run table)Show 7 steps
StepLineStateWhat happens
1L1n=5 edges=5An empty undirected graph on 5 vertices. The three representations below are always updated together: the matrix always has 5 × 5 = 25 cells, the adjacency list grows with the edges, and the edge list is just the edges.
2L10edge=0-1 matrix cells changed=2 list entries=2Add the edge 0-1: both mirror cells [0][1] and [1][0], two list entries and one edge-list entry.
3L10edge=0-2 matrix cells changed=2 list entries=4Add the edge 0-2: both mirror cells [0][2] and [2][0], two list entries and one edge-list entry.
4L10edge=1-3 matrix cells changed=2 list entries=6Add the edge 1-3: both mirror cells [1][3] and [3][1], two list entries and one edge-list entry.
5L10edge=2-3 matrix cells changed=2 list entries=8Add the edge 2-3: both mirror cells [2][3] and [3][2], two list entries and one edge-list entry.
6L10edge=3-4 matrix cells changed=2 list entries=10Add the edge 3-4: both mirror cells [3][4] and [4][3], two list entries and one edge-list entry.
7L13return=[2, 2, 2, 3, 1] sum of degrees=10 edges=5Degrees (row sums) [2, 2, 2, 3, 1]; they add up to 10 = 2 × 5: every edge has two ends (handshaking lemma).
32.03.4

Incidence matrix

The **incidence matrix** has one row per vertex and one column per edge. For an undirected edge, the two rows of its endpoints get 1 in that column; for a directed edge the tail gets -1 and the head +1. Each column of an undirected graph therefore sums to 2 and each column of a directed graph to 0 (for non-loop edges). It is n × m, usually larger than the adjacency matrix for sparse graphs, and is mostly of theoretical use: linear algebra on graphs, flow conservation, cycle spaces.

In flow problems the directed incidence matrix expresses 'flow in equals flow out' at every vertex as one matrix equation. In everyday programming you rarely build it, but knowing it explains the sign conventions you meet in network-flow texts.

Java
1static int[][] incidence(int n, int[][] edges, boolean directed) { // row = vertex, column = edge
2 int[][] inc = new int[n][edges.length];
3 for (int k = 0; k < edges.length; k++) {
4 int a = edges[k][0], b = edges[k][1];
5 if (directed) { inc[a][k] -= 1; inc[b][k] += 1; } // tail -1, head +1
6 else { inc[a][k] += 1; inc[b][k] += 1; } // both ends +1 (a self-loop gets 2)
7 }
8 return inc;
9}
32.03.5

Compressed sparse row (CSR)

**Compressed sparse row** (CSR) stores all neighbour lists back to back in one int[] target, plus an int[] offset where offset[v] is the start of vertex v's block and offset[v + 1] its end. It has no per-vertex objects and no pointers: scanning neighbours is a contiguous read, which is very fast, and the memory is exactly n + 1 + m ints for a directed graph. The price is that it is static: adding an edge means rebuilding.

Building takes two passes: count each vertex's degree into offset[v + 1], turn the counts into start positions with a prefix sum, then place each neighbour in its block. The animation shows the three steps. In the preset "Undirected: every edge twice" the neighbours of vertex 3 are [1, 2, 4], read from target[6..8].

Java
1static int[][] buildCsr(int n, int[][] edges, boolean directed) { // returns {offset, target}
2 int[] offset = new int[n + 1];
3 for (int[] e : edges) { // pass 1: count the out-degree of every vertex
4 offset[e[0] + 1]++;
5 if (!directed) offset[e[1] + 1]++;
6 }
7 for (int v = 0; v < n; v++) offset[v + 1] += offset[v]; // prefix sums: where each block starts
8 int[] target = new int[offset[n]], next = offset.clone();
9 for (int[] e : edges) { // pass 2: write every neighbour into its block
10 target[next[e[0]]++] = e[1];
11 if (!directed) target[next[e[1]]++] = e[0];
12 }
13 return new int[][]{offset, target};
14}

Compressed sparse row (CSR): count, offsets, fill

Interactive · edit the input

Your input

Try

directed graph · 5 vertices · 6 edges

0
1
2
3
4
Being readJust writtenNeighbours of the chosen vertexStored value

offset[v] (start of v's block)

0v=0
0v=1
0v=2
0v=3
0v=4
0v=5
Setup

CSR stores all neighbour lists back to back in one int array, with an offset array saying where each vertex's block starts. Building it takes two passes over the edges and one over the vertices.

Step 1 / 15
Java
1static int[][] buildCsr(int n, int[][] edges, boolean directed) { // returns {offset, target}
2 int[] offset = new int[n + 1];
3 for (int[] e : edges) { // pass 1: count the out-degree of every vertex
4 offset[e[0] + 1]++;
5 if (!directed) offset[e[1] + 1]++;
6 }
7 for (int v = 0; v < n; v++) offset[v + 1] += offset[v]; // prefix sums: where each vertex's block starts
8 int[] target = new int[offset[n]], next = offset.clone();
9 for (int[] e : edges) { // pass 2: write every neighbour into its block
10 target[next[e[0]]++] = e[1];
11 if (!directed) target[next[e[1]]++] = e[0];
12 }
13 return new int[][]{offset, target};
14}
15// the neighbours of v are target[offset[v]] .. target[offset[v + 1] - 1]

Live state

0 array cells

n

5

edges

6

No condition evaluated on this step
Execution trace (dry run table)Show 15 steps
StepLineStateWhat happens
1L2n=5 edges=6CSR stores all neighbour lists back to back in one int array, with an offset array saying where each vertex's block starts. Building it takes two passes over the edges and one over the vertices.
2L4edge=0-1Count edge 0-1: out-degree of 0 grows (stored one slot to the right, at offset[v + 1], so the prefix sum shifts it into place).
3L4edge=0-2Count edge 0-2: out-degree of 0 grows (stored one slot to the right, at offset[v + 1], so the prefix sum shifts it into place).
4L4edge=1-3Count edge 1-3: out-degree of 1 grows (stored one slot to the right, at offset[v + 1], so the prefix sum shifts it into place).
5L4edge=2-3Count edge 2-3: out-degree of 2 grows (stored one slot to the right, at offset[v + 1], so the prefix sum shifts it into place).
6L4edge=3-4Count edge 3-4: out-degree of 3 grows (stored one slot to the right, at offset[v + 1], so the prefix sum shifts it into place).
7L4edge=4-0Count edge 4-0: out-degree of 4 grows (stored one slot to the right, at offset[v + 1], so the prefix sum shifts it into place).
8L7total entries=6Prefix sums turn the counts into start positions: offset = [0, 2, 3, 4, 5, 6]. Vertex v's block is target[offset[v]..offset[v+1]-1], and the target array needs 6 cells.
9L10edge=0-1 writes=1Write edge 0-1: 1 goes into the next free cell of vertex 0's block.
10L10edge=0-2 writes=1Write edge 0-2: 2 goes into the next free cell of vertex 0's block.
11L10edge=1-3 writes=1Write edge 1-3: 3 goes into the next free cell of vertex 1's block.
12L10edge=2-3 writes=1Write edge 2-3: 3 goes into the next free cell of vertex 2's block.
13L10edge=3-4 writes=1Write edge 3-4: 4 goes into the next free cell of vertex 3's block.
14L10edge=4-0 writes=1Write edge 4-0: 0 goes into the next free cell of vertex 4's block.
15L13return=[4] block=[4, 5)Neighbours of 3: target[4..4] = [4]. Reading them is one contiguous scan of 1 cell, with no pointers to chase. The structure cannot be changed cheaply: adding an edge means rebuilding.

Practice ladder

Now try it yourself

Easy

  • Find the Town Judge
  • Find Center of Star Graph

Medium

  • Number of Provinces
  • Clone Graph

Advanced

  • Reconstruct Itinerary
  • Maximum Employees to Be Invited to a Meeting

Key takeaways

  • Matrix: O(1) edge test, O(n) neighbours, n² space. List: O(degree) neighbours, n + m space. Edge list: m space, best for edge-order algorithms.
  • The incidence matrix records which edges touch which vertices (±1 for directed edges).
  • CSR stores every neighbour list in one array with an offset array: compact, fast to scan, static.