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
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.
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.
1class AdjMatrix {2private final int[][] m;3private final boolean directed;4AdjMatrix(int n, boolean directed) { m = new int[n][n]; this.directed = directed; }5void addEdge(int a, int b) {6m[a][b]++;7if (!directed) m[b][a]++; // a self-loop counts twice on the diagonal8}9boolean hasEdge(int a, int b) { return m[a][b] > 0; } // O(1)10List<Integer> neighbours(int v) { // O(n): scan the whole row11List<Integer> out = new ArrayList<>();12for (int u = 0; u < m.length; u++) if (m[v][u] > 0) out.add(u);13return out;14}15}
One graph, three representations: matrix, adjacency list, edge list
Interactive · edit the inputgrid · 6 × 6
undirected graph · 5 vertices · 0 edges
list[0]
list[1]
list[2]
list[3]
list[4]
edge list (a-b, weight)
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.
1int[][] matrix = new int[n][n]; // adjacency matrix: n * n cells2List<List<int[]>> list = new ArrayList<>(); // adjacency list: {neighbour, weight} per vertex3for (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) {6edgeList[k] = new int[]{a, b, w};7matrix[a][b]++; // directed: this one cell is the whole edge8list.get(a).add(new int[]{b, w});9if (!directed) {10matrix[b][a]++; // undirected: also the mirror cell (a self-loop counts twice on the diagonal)11list.get(b).add(new int[]{a, w});12}13}
Live state
n
5
edges
5
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=5 edges=5 | 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. |
| 2 | L10 | edge=0-1 matrix cells changed=2 list entries=2 | Add the edge 0-1: both mirror cells [0][1] and [1][0], two list entries and one edge-list entry. |
| 3 | L10 | edge=0-2 matrix cells changed=2 list entries=4 | Add the edge 0-2: both mirror cells [0][2] and [2][0], two list entries and one edge-list entry. |
| 4 | L10 | edge=1-3 matrix cells changed=2 list entries=6 | Add the edge 1-3: both mirror cells [1][3] and [3][1], two list entries and one edge-list entry. |
| 5 | L10 | edge=2-3 matrix cells changed=2 list entries=8 | Add the edge 2-3: both mirror cells [2][3] and [3][2], two list entries and one edge-list entry. |
| 6 | L10 | edge=3-4 matrix cells changed=2 list entries=10 | Add the edge 3-4: both mirror cells [3][4] and [4][3], two list entries and one edge-list entry. |
| 7 | L13 | return=[2, 2, 2, 3, 1] sum of degrees=10 edges=5 | Degrees (row sums) [2, 2, 2, 3, 1]; they add up to 10 = 2 × 5: every edge has two ends (handshaking lemma). |
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).
1class AdjList {2private final List<List<int[]>> g = new ArrayList<>(); // {neighbour, weight}3private final boolean directed;4AdjList(int n, boolean directed) {5this.directed = directed;6for (int i = 0; i < n; i++) g.add(new ArrayList<>());7}8void addEdge(int a, int b, int w) {9g.get(a).add(new int[]{b, w});10if (!directed) g.get(b).add(new int[]{a, w});11}12List<int[]> neighbours(int v) { return g.get(v); } // O(degree)13int degree(int v) { return g.get(v).size(); }14boolean hasEdge(int a, int b) { // O(degree of a)15for (int[] e : g.get(a)) if (e[0] == b) return true;16return false;17}18}
One graph, three representations: matrix, adjacency list, edge list
Interactive · edit the inputgrid · 6 × 6
undirected graph · 5 vertices · 0 edges
list[0]
list[1]
list[2]
list[3]
list[4]
edge list (a-b, weight)
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.
1int[][] matrix = new int[n][n]; // adjacency matrix: n * n cells2List<List<int[]>> list = new ArrayList<>(); // adjacency list: {neighbour, weight} per vertex3for (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) {6edgeList[k] = new int[]{a, b, w};7matrix[a][b]++; // directed: this one cell is the whole edge8list.get(a).add(new int[]{b, w});9if (!directed) {10matrix[b][a]++; // undirected: also the mirror cell (a self-loop counts twice on the diagonal)11list.get(b).add(new int[]{a, w});12}13}
Live state
n
5
edges
5
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=5 edges=5 | 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. |
| 2 | L10 | edge=0-1 matrix cells changed=2 list entries=2 | Add the edge 0-1: both mirror cells [0][1] and [1][0], two list entries and one edge-list entry. |
| 3 | L10 | edge=0-2 matrix cells changed=2 list entries=4 | Add the edge 0-2: both mirror cells [0][2] and [2][0], two list entries and one edge-list entry. |
| 4 | L10 | edge=1-3 matrix cells changed=2 list entries=6 | Add the edge 1-3: both mirror cells [1][3] and [3][1], two list entries and one edge-list entry. |
| 5 | L10 | edge=2-3 matrix cells changed=2 list entries=8 | Add the edge 2-3: both mirror cells [2][3] and [3][2], two list entries and one edge-list entry. |
| 6 | L10 | edge=3-4 matrix cells changed=2 list entries=10 | Add the edge 3-4: both mirror cells [3][4] and [4][3], two list entries and one edge-list entry. |
| 7 | L13 | return=[2, 2, 2, 3, 1] sum of degrees=10 edges=5 | Degrees (row sums) [2, 2, 2, 3, 1]; they add up to 10 = 2 × 5: every edge has two ends (handshaking lemma). |
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.
1class EdgeList {2final List<int[]> edges = new ArrayList<>(); // {a, b, weight}3void add(int a, int b, int w) { edges.add(new int[]{a, b, w}); }4int[] degrees(int n) { // O(m): one pass over the edges5int[] deg = new int[n];6for (int[] e : edges) { deg[e[0]]++; deg[e[1]]++; }7return deg;8}9List<int[]> sortedByWeight() { // what Kruskal's algorithm starts with10List<int[]> copy = new ArrayList<>(edges);11copy.sort(Comparator.comparingInt(e -> e[2]));12return copy;13}14}
One graph, three representations: matrix, adjacency list, edge list
Interactive · edit the inputgrid · 6 × 6
undirected graph · 5 vertices · 0 edges
list[0]
list[1]
list[2]
list[3]
list[4]
edge list (a-b, weight)
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.
1int[][] matrix = new int[n][n]; // adjacency matrix: n * n cells2List<List<int[]>> list = new ArrayList<>(); // adjacency list: {neighbour, weight} per vertex3for (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) {6edgeList[k] = new int[]{a, b, w};7matrix[a][b]++; // directed: this one cell is the whole edge8list.get(a).add(new int[]{b, w});9if (!directed) {10matrix[b][a]++; // undirected: also the mirror cell (a self-loop counts twice on the diagonal)11list.get(b).add(new int[]{a, w});12}13}
Live state
n
5
edges
5
Execution trace (dry run table)Show 7 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | n=5 edges=5 | 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. |
| 2 | L10 | edge=0-1 matrix cells changed=2 list entries=2 | Add the edge 0-1: both mirror cells [0][1] and [1][0], two list entries and one edge-list entry. |
| 3 | L10 | edge=0-2 matrix cells changed=2 list entries=4 | Add the edge 0-2: both mirror cells [0][2] and [2][0], two list entries and one edge-list entry. |
| 4 | L10 | edge=1-3 matrix cells changed=2 list entries=6 | Add the edge 1-3: both mirror cells [1][3] and [3][1], two list entries and one edge-list entry. |
| 5 | L10 | edge=2-3 matrix cells changed=2 list entries=8 | Add the edge 2-3: both mirror cells [2][3] and [3][2], two list entries and one edge-list entry. |
| 6 | L10 | edge=3-4 matrix cells changed=2 list entries=10 | Add the edge 3-4: both mirror cells [3][4] and [4][3], two list entries and one edge-list entry. |
| 7 | L13 | return=[2, 2, 2, 3, 1] sum of degrees=10 edges=5 | Degrees (row sums) [2, 2, 2, 3, 1]; they add up to 10 = 2 × 5: every edge has two ends (handshaking lemma). |
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.
1static int[][] incidence(int n, int[][] edges, boolean directed) { // row = vertex, column = edge2int[][] inc = new int[n][edges.length];3for (int k = 0; k < edges.length; k++) {4int a = edges[k][0], b = edges[k][1];5if (directed) { inc[a][k] -= 1; inc[b][k] += 1; } // tail -1, head +16else { inc[a][k] += 1; inc[b][k] += 1; } // both ends +1 (a self-loop gets 2)7}8return inc;9}
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].
1static int[][] buildCsr(int n, int[][] edges, boolean directed) { // returns {offset, target}2int[] offset = new int[n + 1];3for (int[] e : edges) { // pass 1: count the out-degree of every vertex4offset[e[0] + 1]++;5if (!directed) offset[e[1] + 1]++;6}7for (int v = 0; v < n; v++) offset[v + 1] += offset[v]; // prefix sums: where each block starts8int[] target = new int[offset[n]], next = offset.clone();9for (int[] e : edges) { // pass 2: write every neighbour into its block10target[next[e[0]]++] = e[1];11if (!directed) target[next[e[1]]++] = e[0];12}13return new int[][]{offset, target};14}
Compressed sparse row (CSR): count, offsets, fill
Interactive · edit the inputdirected graph · 5 vertices · 6 edges
offset[v] (start of v's block)
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.
1static int[][] buildCsr(int n, int[][] edges, boolean directed) { // returns {offset, target}2int[] offset = new int[n + 1];3for (int[] e : edges) { // pass 1: count the out-degree of every vertex4offset[e[0] + 1]++;5if (!directed) offset[e[1] + 1]++;6}7for (int v = 0; v < n; v++) offset[v + 1] += offset[v]; // prefix sums: where each vertex's block starts8int[] target = new int[offset[n]], next = offset.clone();9for (int[] e : edges) { // pass 2: write every neighbour into its block10target[next[e[0]]++] = e[1];11if (!directed) target[next[e[1]]++] = e[0];12}13return new int[][]{offset, target};14}15// the neighbours of v are target[offset[v]] .. target[offset[v + 1] - 1]
Live state
n
5
edges
6
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=5 edges=6 | 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. |
| 2 | L4 | edge=0-1 | Count 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). |
| 3 | L4 | edge=0-2 | Count 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). |
| 4 | L4 | edge=1-3 | Count 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). |
| 5 | L4 | edge=2-3 | Count 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). |
| 6 | L4 | edge=3-4 | Count 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). |
| 7 | L4 | edge=4-0 | Count 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). |
| 8 | L7 | total entries=6 | Prefix 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. |
| 9 | L10 | edge=0-1 writes=1 | Write edge 0-1: 1 goes into the next free cell of vertex 0's block. |
| 10 | L10 | edge=0-2 writes=1 | Write edge 0-2: 2 goes into the next free cell of vertex 0's block. |
| 11 | L10 | edge=1-3 writes=1 | Write edge 1-3: 3 goes into the next free cell of vertex 1's block. |
| 12 | L10 | edge=2-3 writes=1 | Write edge 2-3: 3 goes into the next free cell of vertex 2's block. |
| 13 | L10 | edge=3-4 writes=1 | Write edge 3-4: 4 goes into the next free cell of vertex 3's block. |
| 14 | L10 | edge=4-0 writes=1 | Write edge 4-0: 0 goes into the next free cell of vertex 4's block. |
| 15 | L13 | return=[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.