// STUHUB · C++ DATA STRUCTURES
Representing a Graph in C++: Adjacency Matrix vs Adjacency List, and Degree in Directed Graphs
How to actually store a graph in C++: build an n×n adjacency matrix and a per-vertex adjacency list from the same edge list, see what each one costs (O(V²) space with O(1) edge lookup against O(V+E) space with O(deg) lookup), and count in-degree, out-degree and self-loops in a directed graph without reversing an arrow. Compilable programs, weighted variants, matrix-to-list conversion, and three worked exercises.
Introduction
Every graph traversal you have ever read starts the same way: "given the adjacency list below…". The list is handed to you already built, and the interesting part — the part that decides whether your BFS runs in O(V+E) or O(V²), and the part that is genuinely easy to get wrong when the edges have arrows on them — is skipped.
This page is that skipped part. A graph is an abstract object: a set of vertices and a set of edges. Before any algorithm can touch it, you have to choose a concrete layout in memory, and there are exactly two layouts worth knowing. One is a grid with a cell for every ordered pair of vertices. The other is one list per vertex holding only the neighbours that exist. They store the same graph, they answer different questions at different speeds, and the choice between them is the reason complexity tables have two columns.
The second half is about direction. In an undirected graph an edge is a fact about two vertices and both of them record it. In a directed graph an edge is a fact about an ordered pair, exactly one vertex records it, and you need to be able to say instantly which end of the arrow is the source and which is the destination — because in-degree and out-degree are counted from opposite ends, and a self-loop contributes to both.
Every program on this page compiles cleanly under g++ -std=c++17 -Wall -Wextra and the printed output shown is the output it produced.
A graph is a set of vertices and a set of edges
Formally, a graph is a pair G = (V, E): a finite set of vertices V, and a set of edges E where each edge joins a pair of vertices. That is the whole definition, and everything else is a qualifier on it.
A graph is non-linear, like a tree — but unlike a tree it has no root, no notion of parent and child, and no rule against cycles. A tree is a special case of a graph, not the other way round. Trees model hierarchies; graphs model general relationships: road networks, social follows, dependencies between build targets, web pages linking to web pages.
The vocabulary below is small and every term earns its place. Learn it once and the rest of the page reads itself.
| Term | What it means |
|---|---|
| Vertex (node) | An individual point in the graph — one entity: a city, a person, a task. |
| Edge (arc, link) | A connection between two vertices. May carry a weight. |
| Directed graph (digraph) | Every edge has a direction: (u, v) means u → v and nothing about v → u. E is a set of ordered pairs. |
| Undirected graph | Edges have no direction: (u, v) and (v, u) are the same edge and it can be traversed both ways. |
| Weighted graph | Every edge carries a numeric value — cost, distance, time, capacity. |
| Unweighted graph | Edges record only that a connection exists. |
| Adjacent | Two vertices joined directly by an edge. In a digraph this word is ambiguous — see the section on direction below. |
| Incident | The relation between an edge and its endpoints: edge (a, b) is incident to a and to b. Vertices are adjacent to vertices; edges are incident to vertices. |
Degree deg(v) | Undirected: the number of edges incident to v. |
In-degree d⁻(v) | Directed: the number of edges arriving at v (arrowheads pointing in). |
Out-degree d⁺(v) | Directed: the number of edges leaving v (arrow tails at v). |
| Path | A sequence of vertices in which each consecutive pair is joined by an edge. |
| Simple path | A path in which no vertex repeats (except possibly the first and last). |
| Cycle | A path that starts and ends at the same vertex. |
| Self-loop | A single edge from a vertex to itself, v → v. |
| Connected graph | Undirected: a path exists between every pair of vertices. |
| Strongly connected digraph | Directed: every vertex can reach every other vertex following the arrows. |
| Weakly connected digraph | Becomes connected only once you ignore the directions. |
| Complete graph | Every vertex is directly joined to every other. Undirected, that is n(n−1)/2 edges. |
| Subgraph | A graph H with V(H) ⊆ V(G) and E(H) ⊆ E(G). |
| Sparse graph | E is far smaller than V² — most possible edges are absent. |
| Dense graph | E is close to V² — most possible edges are present. |
| Spanning tree | A cycle-free subgraph touching every vertex, using exactly V − 1 edges. |
| DAG | Directed Acyclic Graph — a digraph with no directed cycle. The precondition for topological sorting. |
Two of these are not descriptions but decisions you make about your own data: sparse and dense. They are the entire basis for choosing between the two representations, so it is worth being concrete. A social network with ten million users and an average of two hundred friends each is sparse — E ≈ 2×10⁹ against a possible V² = 10¹⁴. A five-city travel-time table where every city has a time to every other is dense; in fact it is complete.
Path, cycle and self-loop are three different things
These three get swapped for each other constantly, and they are not interchangeable.
- A path is a sequence:
A, B, E, Gis a path if each consecutive pair is joined by an edge. It has a length (usually counted in edges) and it goes from somewhere to somewhere else. - A cycle is a path whose first and last vertex are the same.
B, D, G, E, Bis a cycle. It involves several vertices and several edges. "A path that starts and ends at the same vertex" is the definition to be able to write down. - A self-loop (sometimes just "loop") is a single edge from a vertex to itself:
U → U. One edge, one vertex. It is not a cycle in the interesting sense and it is not a path.
The distinction that matters in practice: a cycle is a property of how the edges are arranged, so it is what makes a traversal need a visited array (without one, BFS or DFS on a cyclic graph loops forever). A self-loop is a single entry in your data structure — one diagonal cell in the matrix, one entry in a vertex's own list — and its consequence is arithmetic: it counts twice towards that vertex's degree.
One more pair that is easy to mix up. Vertices are adjacent to vertices; edges are incident to vertices. If you are describing the relationship between an edge and one of its endpoints, the word is incident.
Which end of the arrow is which
In a directed graph, edge (u, v) means u → v. The two ends have names, and they come in matched sets:
The tail of the arrow, u | The head of the arrow, v |
|---|---|
| origin | destination |
| source | target |
predecessor of v | successor of u |
| the vertex the edge leaves | the vertex the edge arrives at |
counts towards d⁺(u), out-degree | counts towards d⁻(v), in-degree |
written first in the pair (u, v) | written second in the pair (u, v) |
fills matrix cell row u | fills matrix cell column v |
stored in u's adjacency list | not stored anywhere else |
The useful anchor is the ordered pair itself. Write the edge down as (u, v) — first position, second position — and every row of that table follows. Origin is the first coordinate. When a question hands you an edge label and asks for its origin and destination, find the labelled arrow, write it out as X → Y, and read off X as origin and Y as destination. Do not try to infer it from where the vertices sit on the page; a diagram is free to draw the destination above, below or to the left of the source.
The same first-position/second-position rule is how you count degrees without drawing anything, which is the subject of a later section: out-degree of v is how many times v appears in the first slot across the edge list, in-degree is how many times it appears in the second.
The adjacency matrix: one cell per ordered pair
Number the vertices 0 … n−1. Allocate an n × n array A. Then A[i][j] records whether there is an edge from i to j.
| Graph is… | What goes in A[i][j] |
|---|---|
| unweighted | 1 if the edge exists, 0 if not |
| weighted | the weight w; for "no edge" use a sentinel the weights cannot be — -1 if all weights are positive, or ∞/a large value if negatives are possible. 0 is a fine sentinel too unless zero is a legal weight |
| undirected | symmetric: an edge fills both A[i][j] and A[j][i] |
| directed | not symmetric: edge (u, v) fills only A[u][v] |
self-loop on v | the diagonal cell A[v][v] |
For the undirected graph A-B, A-C, B-D, B-E, C-E, C-F, D-G, E-G, F-G the matrix is:
| A | B | C | D | E | F | G | |
|---|---|---|---|---|---|---|---|
| A | 0 | 1 | 1 | 0 | 0 | 0 | 0 |
| B | 1 | 0 | 0 | 1 | 1 | 0 | 0 |
| C | 1 | 0 | 0 | 0 | 1 | 1 | 0 |
| D | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
| E | 0 | 1 | 1 | 0 | 0 | 0 | 1 |
| F | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
| G | 0 | 0 | 0 | 1 | 1 | 1 | 0 |
Three checks you can run on any undirected matrix in about five seconds, and should:
- It is symmetric about the main diagonal. If it is not, either you have missed a mirrored cell or the graph was directed after all.
- The number of 1s is exactly
2|E|. Here 18, for nine edges. Every undirected edge is recorded twice. - The diagonal is all zeros unless the graph has self-loops.
And the property that turns the matrix into a degree calculator: the sum of row i is the degree of vertex i (out-degree, if the graph is directed) and the sum of column j is its in-degree. In the matrix above, row B sums to 3, and B does have three neighbours.
What the matrix buys you is the cell lookup. "Is there an edge from i to j?" is one array read — O(1), independent of how large or how dense the graph is. What it costs you is everything else: n² cells whether or not the edges exist, and O(V) work to list one vertex's neighbours because you have to scan the whole row, most of which is zeros.
The adjacency list: one list per vertex
Instead of a cell for every possible edge, give every vertex a list of the neighbours it actually has. Classically that is a linked list — a node per neighbour holding a vertex field and a link to the next node, plus a weight field if the graph is weighted. In modern C++ you would write std::vector<std::vector<int>>, which is the same structure with better cache behaviour and no manual delete.
The same graph:
A -> B, C
B -> A, D, E
C -> A, E, F
D -> B, G
E -> B, C, G
F -> C, G
G -> D, E, FThe rules mirror the matrix:
- Undirected: each edge appears twice, once in each endpoint's list. So the total number of list entries is
2|E|— 18 above, matching the count of 1s in the matrix, because they are literally the same 18 facts. - Directed: each edge appears once, in the list of its source. A vertex's list is its successors. The total number of entries is
|E|. - Self-loop on
v:vappears in its own list. Forgetting this is the most common list-building slip on directed graphs. - A vertex with no neighbours gets an empty list. When you are writing one out by hand, write something visible — a dash — rather than leaving a blank that reads as an omission.
- Order within a list is not part of the graph.
B -> A, D, EandB -> E, D, Adescribe the same graph. But it is part of the traversal: BFS and DFS visit neighbours in list order, so if you are asked to trace a traversal in ascending order, sort the lists first and do it once, carefully, before you start.
What the list buys you is proportionality: space is O(V + E) — the V is for the array of heads, which you pay for even for isolated vertices — and enumerating a vertex's neighbours costs O(deg(v)), touching nothing that is not an actual edge. What it costs you is the cell lookup: "is there an edge i → j?" now means scanning i's list, O(deg(i)), up to O(V) in the worst case.
Both representations, built from one edge list
Here is the whole idea in one program. It takes a single edge list and produces both structures from it, then prints them and checks them against each other. Read the two build functions side by side: the matrix writes two cells per edge, the list appends two entries per edge, and those are the same two facts stored two ways.
Note the two hasEdge functions at the bottom. They answer the same question and their bodies tell you the whole trade-off: one is an array read, the other is a loop.
Output:
Adjacency matrix
A B C D E F G
A 0 1 1 0 0 0 0
B 1 0 0 1 1 0 0
C 1 0 0 0 1 1 0
D 0 1 0 0 0 0 1
E 0 1 1 0 0 0 1
F 0 0 1 0 0 0 1
G 0 0 0 1 1 1 0
Adjacency list
A -> B, C
B -> A, D, E
C -> A, E, F
D -> B, G
E -> B, C, G
F -> C, G
G -> D, E, F
Degree (row sum of the matrix == length of the list)
deg(A) = 2 list length = 2
deg(B) = 3 list length = 3
deg(C) = 3 list length = 3
deg(D) = 2 list length = 2
deg(E) = 3 list length = 3
deg(F) = 2 list length = 2
deg(G) = 3 list length = 3
total = 18 and 18 = 2|E| = 18
Edge lookup B-E: matrix 1, list 1
Edge lookup B-F: matrix 0, list 0#include <algorithm>
#include <iostream>
#include <string>
#include <utility>
#include <vector>
// One undirected, unweighted graph, built twice from the same edge list.
// A-B A-C B-D B-E C-E C-F D-G E-G F-G |V| = 7, |E| = 9
const std::vector<std::string> NAME = {"A", "B", "C", "D", "E", "F", "G"};
const int V = 7;
const std::vector<std::pair<int, int>> EDGES = {
{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 4}, {2, 5}, {3, 6}, {4, 6}, {5, 6}};
// ---- Representation 1: adjacency matrix. V*V cells, allocated up front. ----
std::vector<std::vector<int>> buildMatrix() {
std::vector<std::vector<int>> a(V, std::vector<int>(V, 0));
for (const auto& e : EDGES) {
a[e.first][e.second] = 1;
a[e.second][e.first] = 1; // undirected: fill BOTH cells
}
return a;
}
// ---- Representation 2: adjacency list. One list per vertex. ----
std::vector<std::vector<int>> buildList() {
std::vector<std::vector<int>> adj(V);
for (const auto& e : EDGES) {
adj[e.first].push_back(e.second);
adj[e.second].push_back(e.first); // undirected: append TWICE
}
for (auto& row : adj) std::sort(row.begin(), row.end());
return adj;
}
// O(1): one array read, no matter how big the graph is.
bool hasEdgeMatrix(const std::vector<std::vector<int>>& a, int i, int j) {
return a[i][j] != 0;
}
// O(deg(i)): scan i's neighbour list.
bool hasEdgeList(const std::vector<std::vector<int>>& adj, int i, int j) {
for (int u : adj[i])
if (u == j) return true;
return false;
}
int main() {
std::vector<std::vector<int>> a = buildMatrix();
std::vector<std::vector<int>> adj = buildList();
std::cout << "Adjacency matrix\n ";
for (int j = 0; j < V; ++j) std::cout << NAME[j] << " ";
std::cout << "\n";
for (int i = 0; i < V; ++i) {
std::cout << " " << NAME[i] << " ";
for (int j = 0; j < V; ++j) std::cout << a[i][j] << " ";
std::cout << "\n";
}
std::cout << "\nAdjacency list\n";
for (int i = 0; i < V; ++i) {
std::cout << " " << NAME[i] << " -> ";
for (std::size_t k = 0; k < adj[i].size(); ++k)
std::cout << NAME[adj[i][k]] << (k + 1 < adj[i].size() ? ", " : "");
std::cout << "\n";
}
std::cout << "\nDegree (row sum of the matrix == length of the list)\n";
int sumRow = 0, sumLen = 0;
for (int i = 0; i < V; ++i) {
int row = 0;
for (int j = 0; j < V; ++j) row += a[i][j];
sumRow += row;
sumLen += static_cast<int>(adj[i].size());
std::cout << " deg(" << NAME[i] << ") = " << row
<< " list length = " << adj[i].size() << "\n";
}
std::cout << " total = " << sumRow << " and " << sumLen
<< " = 2|E| = " << 2 * EDGES.size() << "\n";
std::cout << "\nEdge lookup B-E: matrix " << hasEdgeMatrix(a, 1, 4)
<< ", list " << hasEdgeList(adj, 1, 4) << "\n";
std::cout << "Edge lookup B-F: matrix " << hasEdgeMatrix(a, 1, 5)
<< ", list " << hasEdgeList(adj, 1, 5) << "\n";
}The trade-off, and why every traversal bound has two versions
| Adjacency matrix | Adjacency list | |
|---|---|---|
| Space | O(V²) | O(V + E) |
Is there an edge (i, j)? | O(1) | O(deg(i)), worst case O(V) |
List all neighbours of v | O(V) — scan the whole row | O(deg(v)) |
| Add an edge | O(1) | O(1) at the head of the list |
| Remove an edge | O(1) | O(deg) — you have to find it first |
| Iterate over every edge | O(V²) | O(V + E) |
| Add a vertex | O(V²) — reallocate the grid | O(1) — push one empty list |
| Best for | dense, near-complete graphs; heavy edge-existence querying | sparse graphs, E ≪ V²; traversal-heavy work |
| Weakness | wastes memory on a sparse graph — mostly zeros | pointer/vector overhead per entry; slow edge tests |
Why BFS and DFS are O(V + E) on a list. A traversal does two things: it visits each vertex once, and at each vertex it walks that vertex's neighbour list. The first is V units of work. The second, summed over all vertices, is Σ deg(v) — which is 2E for an undirected graph and E for a directed one, so O(E) either way. Total O(V + E). Nothing in that argument depends on the graph being sparse; the bound is exact.
Why the same code is O(V²) on a matrix. Change one thing — the neighbour loop now scans a row of V cells instead of a list of deg(v) entries — and the second term becomes V per vertex, V² overall, regardless of how few edges there are. On a sparse graph that is not a small constant factor: with V = 10⁵ and E = 3×10⁵, V + E is about 4×10⁵ and V² is 10¹⁰. The matrix version does twenty-five thousand times the work, and needs 10¹⁰ cells to hold a graph with 3×10⁵ edges, which it cannot.
When the matrix genuinely wins. When E is close to V², O(V²) space is O(E) space and you are paying nothing for the zeros — plus you get contiguous memory, no indirection, and a bit-per-edge encoding if you want it. When the dominant operation is "is i connected to j?" rather than "who are i's neighbours?" — think a fixed 200-airport route table queried millions of times — the O(1) lookup is worth the grid. And when V is small and fixed, V² is small and fixed; nobody worries about a 50×50 array.
The default, in the absence of a reason, is the adjacency list. Real graphs are overwhelmingly sparse.
Directed graphs: the same code, minus the mirroring
Everything about direction reduces to one edit in each builder. The matrix writes one cell instead of two. The list appends one entry instead of two. That is it — and it is also why the total storage halves, and why the matrix stops being symmetric.
The program below uses the digraph 0→1, 0→2, 1→2, 2→2, 2→3, 3→1, 3→4, 4→0, which has a self-loop on vertex 2. Watch three things in the output: the matrix is not symmetric, A[2][2] is the self-loop sitting on the diagonal, and the row sums and column sums reproduce the out-degrees and in-degrees exactly.
Output:
Adjacency matrix (row = from, column = to)
0 1 2 3 4
0 0 1 1 0 0
1 0 0 1 0 0
2 0 0 1 1 0
3 0 1 0 0 1
4 1 0 0 0 0
Adjacency list (successors only)
0 -> 1, 2
1 -> 2
2 -> 2, 3
3 -> 1, 4
4 -> 0
v out in degree (out = row sum, in = column sum)
0 2 1 3 (row 2, col 1)
1 1 2 3 (row 1, col 2)
2 2 3 5 (row 2, col 3)
3 2 1 3 (row 2, col 1)
4 1 1 2 (row 1, col 1)
sum of out-degrees = 8, sum of in-degrees = 8, |E| = 8
vertex 2 carries a self-loop: out 1 + in 1, so it adds 2 to degree(2)#include <iostream>
#include <utility>
#include <vector>
// One DIRECTED graph, with a self-loop on vertex 2.
// 0->1 0->2 1->2 2->2 2->3 3->1 3->4 4->0 |V| = 5, |E| = 8
const int V = 5;
const std::vector<std::pair<int, int>> EDGES = {
{0, 1}, {0, 2}, {1, 2}, {2, 2}, {2, 3}, {3, 1}, {3, 4}, {4, 0}};
int main() {
std::vector<std::vector<int>> a(V, std::vector<int>(V, 0));
std::vector<std::vector<int>> succ(V); // out-edges only
std::vector<int> outDeg(V, 0), inDeg(V, 0);
for (const auto& e : EDGES) {
int from = e.first, to = e.second; // from --> to
a[from][to] = 1; // ONE cell, not two
succ[from].push_back(to); // ONE list, not two
++outDeg[from]; // tail of the arrow
++inDeg[to]; // head of the arrow
}
std::cout << "Adjacency matrix (row = from, column = to)\n ";
for (int j = 0; j < V; ++j) std::cout << j << " ";
std::cout << "\n";
for (int i = 0; i < V; ++i) {
std::cout << " " << i << " ";
for (int j = 0; j < V; ++j) std::cout << a[i][j] << " ";
std::cout << "\n";
}
std::cout << "\nAdjacency list (successors only)\n";
for (int i = 0; i < V; ++i) {
std::cout << " " << i << " -> ";
if (succ[i].empty()) std::cout << "-";
for (std::size_t k = 0; k < succ[i].size(); ++k)
std::cout << succ[i][k] << (k + 1 < succ[i].size() ? ", " : "");
std::cout << "\n";
}
std::cout << "\n v out in degree (out = row sum, in = column sum)\n";
int sumOut = 0, sumIn = 0;
for (int v = 0; v < V; ++v) {
int row = 0, col = 0;
for (int j = 0; j < V; ++j) row += a[v][j]; // row sum = out-degree
for (int i = 0; i < V; ++i) col += a[i][v]; // col sum = in-degree
sumOut += outDeg[v];
sumIn += inDeg[v];
std::cout << " " << v << " " << outDeg[v] << " " << inDeg[v]
<< " " << outDeg[v] + inDeg[v]
<< " (row " << row << ", col " << col << ")\n";
}
std::cout << "\n sum of out-degrees = " << sumOut
<< ", sum of in-degrees = " << sumIn
<< ", |E| = " << EDGES.size() << "\n";
std::cout << " vertex 2 carries a self-loop: out 1 + in 1, so it adds 2 "
"to degree(2)\n";
}Counting degree by hand, and the check that catches your mistake
You will not always have a compiler. The reliable manual procedure works straight off the edge list, with no diagram:
- Out-degree
d⁺(v): count how many pairs havevin the first slot —(v, _). - In-degree
d⁻(v): count how many pairs havevin the second slot —(_, v). - Total degree of
vin a digraph isd⁺(v) + d⁻(v).
Do it column-wise, one vertex per row of a scratch table, and write the actual edges into the cells rather than just a tally — you can then re-read them instead of re-counting.
The self-loop. An edge v → v puts v in the first slot and the second slot. So it adds one to out-degree and one to in-degree, and therefore two to the total degree of v. In the matrix it is the single diagonal cell A[v][v], which is counted once by the row sum and once by the column sum — the same two contributions, arrived at from the other direction. In the adjacency list, v appears in its own list. Undirected graphs use the same rule for the same reason: a self-loop contributes 2 to deg(v).
The check, and it takes ten seconds. Every directed edge has exactly one tail and exactly one head, so
Σ d⁺(v) = Σ d⁻(v) = |E|
Add up your out-degree column. Add up your in-degree column. Both must equal the number of edges. If either is off, you have miscounted, missed a self-loop, or reversed an arrow — and you know it before you use the numbers for anything. In the digraph above, both sums are 8 and there are 8 edges.
The undirected version of the same statement is the handshake lemma: Σ deg(v) = 2|E|, because each undirected edge is incident to two vertex-ends. That is the 18-for-9-edges check from the matrix section. (Both statements are really the same counting argument, so in a digraph the total degrees also sum to 2|E| — but the useful check there is the sharper one, Σ d⁺ = Σ d⁻ = |E|, because it will catch a reversed arrow, which the combined sum will not.)
One consequence worth knowing because it makes questions trivial: since Σ deg(v) = 2|E| is even, the number of odd-degree vertices in an undirected graph is always even. If you count degrees and end up with three odd ones, you have made an arithmetic error, not discovered a strange graph.
Weights, and converting between the two forms
Adding weights changes nothing structural. The matrix stores the weight in place of the 1, with a sentinel value standing in for "no edge". The list stores a {neighbour, weight} pair per entry instead of a bare vertex index.
The only real decision is the sentinel. 0 is the natural choice and is wrong the moment a zero-cost edge is legal. -1 works when all weights are positive and is compact to write by hand. ∞ — in practice INT_MAX or a value larger than any possible path — is what shortest-path code wants, because it makes dist + w comparisons behave, though you then have to watch for overflow when you add to it. Pick one, and say which one you picked.
Matrix → list. Scan row i left to right; for every cell that is not the sentinel, append j to vertex i's list. Because j increases as you scan, each list comes out already sorted ascending — you never need a sort step. That is worth remembering when you are asked for adjacency lists "in ascending order" and you have been given a matrix: just read the rows left to right.
List → matrix. Fill the whole grid with the sentinel first, then walk every list and set A[i][j]. If the graph is undirected and the lists were written out in full (each edge in both endpoints' lists), you will set both mirrored cells naturally; if you were given only one direction per edge, you must set A[j][i] yourself.
The checks after any conversion:
| Check | Undirected | Directed |
|---|---|---|
| Matrix symmetric? | must be | usually not |
| Non-sentinel cells | 2 × (number of edges) | one per edge |
| Total adjacency-list entries | 2 × (number of edges) | one per edge |
Non-sentinel cells in row i | deg(i) | d⁺(i) |
Non-sentinel cells in column j | deg(j) | d⁻(j) |
The program below builds a weight matrix for the weighted version of our seven-vertex graph, prints it, converts it row by row into a weighted adjacency list, and confirms the entry count.
Output:
Weight matrix (-1 means no edge)
A B C D E F G
A -1 3 2 -1 -1 -1 -1
B 3 -1 -1 4 1 -1 -1
C 2 -1 -1 -1 5 2 -1
D -1 4 -1 -1 -1 -1 3
E -1 1 5 -1 -1 -1 4
F -1 -1 2 -1 -1 -1 6
G -1 -1 -1 3 4 6 -1
Weighted adjacency list
A -> (B,3) (C,2)
B -> (A,3) (D,4) (E,1)
C -> (A,2) (E,5) (F,2)
D -> (B,4) (G,3)
E -> (B,1) (C,5) (G,4)
F -> (C,2) (G,6)
G -> (D,3) (E,4) (F,6)
list nodes = 18 = 2|E| = 18#include <iostream>
#include <string>
#include <utility>
#include <vector>
// A WEIGHTED undirected graph, stored as a weight matrix, then converted
// row by row into a weighted adjacency list.
// A-B(3) A-C(2) B-D(4) B-E(1) C-E(5) C-F(2) D-G(3) E-G(4) F-G(6)
const std::vector<std::string> NAME = {"A", "B", "C", "D", "E", "F", "G"};
const int V = 7;
const int NO_EDGE = -1; // safe only because every real weight is positive
struct WEdge { int to; int weight; };
int main() {
const std::vector<std::vector<int>> raw = {
{0, 1, 3}, {0, 2, 2}, {1, 3, 4}, {1, 4, 1}, {2, 4, 5},
{2, 5, 2}, {3, 6, 3}, {4, 6, 4}, {5, 6, 6}};
std::vector<std::vector<int>> w(V, std::vector<int>(V, NO_EDGE));
for (const auto& e : raw) {
w[e[0]][e[1]] = e[2];
w[e[1]][e[0]] = e[2];
}
std::cout << "Weight matrix (-1 means no edge)\n ";
for (int j = 0; j < V; ++j) std::cout << " " << NAME[j];
std::cout << "\n";
for (int i = 0; i < V; ++i) {
std::cout << " " << NAME[i] << " ";
for (int j = 0; j < V; ++j) {
std::cout << (w[i][j] == NO_EDGE ? " -1" : " " + std::to_string(w[i][j]));
}
std::cout << "\n";
}
// Matrix -> list. Scanning j in increasing order makes each list sorted
// for free; there is nothing to sort afterwards.
std::vector<std::vector<WEdge>> adj(V);
for (int i = 0; i < V; ++i)
for (int j = 0; j < V; ++j)
if (w[i][j] != NO_EDGE) adj[i].push_back(WEdge{j, w[i][j]});
std::cout << "\nWeighted adjacency list\n";
int nodes = 0;
for (int i = 0; i < V; ++i) {
std::cout << " " << NAME[i] << " -> ";
for (std::size_t k = 0; k < adj[i].size(); ++k) {
std::cout << "(" << NAME[adj[i][k].to] << "," << adj[i][k].weight << ")"
<< (k + 1 < adj[i].size() ? " " : "");
++nodes;
}
std::cout << "\n";
}
std::cout << " list nodes = " << nodes << " = 2|E| = " << 2 * raw.size() << "\n";
}"Adjacent to" is ambiguous in a directed graph — say which you mean
In an undirected graph, "u is adjacent to v" is unambiguous and symmetric: there is an edge between them, full stop.
In a directed graph it is not, and sources genuinely disagree. Given the edge u → v:
- One tradition says
uis adjacent tov, andvis adjacent fromu— the preposition carries the direction, and "the vertices adjacent tov" therefore meansv's predecessors, the vertices with arrows pointing at it. - The other tradition — the one most C++ and algorithms texts use, and the one implied by the phrase "adjacency list" — says the vertices adjacent to
uare the onesupoints at: its successors.
Both are in print. This is not a subtlety you can resolve by thinking harder about it; it is a naming collision. What you can do is:
- Look for the definition in front of you. If the material you are working from defines it, that definition wins for that material, whatever any other book says.
- Watch for a parenthetical. A question that says "adjacent (successor) vertices" has told you which one it means, and that overrides everything.
- When you are the one writing, do not use the bare word. Write successors of `u`, or predecessors of `v`, or out-neighbours and in-neighbours. Nobody has ever been confused by "out-neighbours".
One thing is stable across both traditions and worth holding onto: an adjacency list stores successors. When you are asked to build the adjacency list of a digraph, each vertex's row is the set of vertices it points at. If you need the predecessors — and you sometimes do, for example when you want the in-degrees, or when you want to walk a dependency graph backwards — you build the reverse graph, which is exercise 3.
The mistakes people actually make
Building the structures
- Filling one matrix cell for an undirected edge.
A[i][j] = 1withoutA[j][i] = 1silently gives you a directed graph. The tell is that the matrix is not symmetric; check for symmetry every single time. - Filling two cells for a directed edge. The mirror-image mistake, and worse, because it invents edges. Now every arrow is two-way,
Σd⁺comes out at2|E|, and any shortest-path answer is wrong in a way that still looks plausible. - Appending an undirected edge to only one list. Same bug, list-flavoured. If your entry count is
|E|and the graph is undirected, you have written each edge once and half the graph is unreachable from the other half. - Dropping the self-loop out of a vertex's own list.
2 → 2means2appears in2's successor list. It looks redundant, so it gets dropped, and then the degree count is wrong by two. - Forgetting the
VinO(V + E). The adjacency list needs an array ofVheads whether or not the vertices have any edges, so an edgeless graph on a million vertices still costs a million pointers. The space isO(V + E), neverO(E). - Using
0as the "no edge" sentinel in a weighted matrix that allows zero weights. A free edge and a missing edge become indistinguishable. Use a value the weights cannot take, and write down which. - Assuming the neighbour lists are sorted. They are only sorted if you sorted them or built them by scanning matrix rows. If a traversal must visit neighbours in ascending order, sort first — the traversal will not do it for you.
Direction and degree
- Reversing an arrow when reading a diagram. The arrowhead is the destination. Write each edge out as an ordered pair
(from, to)before you count anything, and count from the pairs, not from the picture. - Reading a labelled edge's origin and destination off the layout. Vertical or leftward arrows are common; position on the page means nothing. Find the arrow, write
X → Y, origin isX. - Swapping row and column. In a directed matrix the convention is row = from, column = to, so row sum is out-degree and column sum is in-degree. Getting this backwards flips every degree in the graph. Sanity check on one vertex you know.
- Counting a self-loop once. It is one edge but two ends, so it contributes 1 to out-degree, 1 to in-degree, and 2 to total degree.
- Not running
Σ d⁺ = Σ d⁻ = |E|. Ten seconds, and it catches missed edges, doubled edges and reversed arrows. The undirected equivalent isΣ deg = 2|E|. - Treating "adjacent to" as self-evident in a digraph. It is not; see the previous section. Say successors or predecessors.
- Using a matrix for a large sparse graph out of habit.
V = 10⁵needs 10¹⁰ cells. The program will not run out of correctness, it will run out of memory. WhenE ≪ V², use the list.
Exercises with solutions
Work through each question before opening the solution below it.
Exercise 1
Build both representations, then check them. An undirected, unweighted graph has vertices 0..5 and edges
0-1, 0-2, 1-3, 1-4, 2-4, 3-4, 3-5, 4-5
- Write the 6×6 adjacency matrix.
- Write the adjacency list, neighbours in ascending order.
- Give
deg(v)for every vertex, and verify the handshake lemma. - State how many bytes each representation would need if you stored the matrix as
int A[6][6]and the list as oneintper entry plus one pointer per vertex (takeint= 4 bytes, pointer = 8). At roughly what edge count would the matrix become the smaller of the two?
Solution
1. Adjacency matrix. Eight edges, so sixteen 1s, symmetric, zero diagonal.
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| 0 | 0 | 1 | 1 | 0 | 0 | 0 |
| 1 | 1 | 0 | 0 | 1 | 1 | 0 |
| 2 | 1 | 0 | 0 | 0 | 1 | 0 |
| 3 | 0 | 1 | 0 | 0 | 1 | 1 |
| 4 | 0 | 1 | 1 | 1 | 0 | 1 |
| 5 | 0 | 0 | 0 | 1 | 1 | 0 |
2. Adjacency list.
0 -> 1, 2
1 -> 0, 3, 4
2 -> 0, 4
3 -> 1, 4, 5
4 -> 1, 2, 3, 5
5 -> 3, 4Sixteen entries — the same sixteen 1s from the matrix, stored the other way round.
3. Degrees. Read them as row sums or as list lengths; they must agree.
| v | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
deg(v) | 2 | 3 | 2 | 3 | 4 | 2 |
Sum = 16 = 2|E| = 2 × 8. ✓ And the odd-degree vertices are 1 and 3 — two of them, an even count, as the lemma forces.
4. Bytes, and the crossover. The matrix is fixed: 6 × 6 × 4 = 144 bytes, and it stays 144 whether the graph has 1 edge or all 15. The list is 6 × 8 = 48 bytes of heads plus 2E × 4 = 64 bytes of entries = 112 bytes. The list wins here, but not by much — this graph is small and fairly dense (8 of a possible 15 edges).
The crossover: the matrix costs 4V²; the list costs 8V + 8E (two 4-byte entries per undirected edge). Setting them equal with V = 6: 144 = 48 + 8E, so E = 12. Past twelve of the fifteen possible edges the matrix is actually the smaller structure — and it is also the faster one, since it answers edge queries in O(1).
That is the whole argument in miniature. The matrix's cost does not depend on E at all, so its relative position improves as the graph fills up; on a sparse graph, where E is a tiny fraction of V², it loses by orders of magnitude rather than by 20 percent. Note also that the real list is worse than this idealised count — a std::vector<std::vector<int>> carries three pointers per inner vector plus allocator overhead, and a linked list carries an 8-byte next in every node, doubling the per-entry cost.
Exercise 2
In-degree, out-degree and a self-loop. A directed graph has vertices A..E and edges
A→B, A→D, B→C, C→A, C→C, C→E, D→B, D→C
- Write the adjacency matrix with row = from, column = to.
- Write the adjacency list of successors, ascending, using a dash for a vertex with none.
- Tabulate
d⁺,d⁻and total degree for every vertex, and run the sum check. - What is
degree(C), and which single edge is responsible for the part of it that people get wrong? - Which vertices are the origin and the destination of the edge
C→E?
Solution
1. Adjacency matrix. Eight edges → eight 1s, one per edge, no mirroring. The matrix is not symmetric, and A[C][C] = 1 is the self-loop.
| A | B | C | D | E | |
|---|---|---|---|---|---|
| A | 0 | 1 | 0 | 1 | 0 |
| B | 0 | 0 | 1 | 0 | 0 |
| C | 1 | 0 | 1 | 0 | 1 |
| D | 0 | 1 | 1 | 0 | 0 |
| E | 0 | 0 | 0 | 0 | 0 |
2. Successor lists. E has no out-edges, so its row is a dash, not a blank.
A -> B, D
B -> C
C -> A, C, E
D -> B, C
E -> -Note C appears in its own list. Eight entries in total, equal to |E| — a directed edge is stored once.
3. Degrees. Out-degree counts appearances in the first slot of an edge; in-degree counts appearances in the second.
| v | out-edges | d⁺ | in-edges | d⁻ | degree |
|---|---|---|---|---|---|
| A | A→B, A→D | 2 | C→A | 1 | 3 |
| B | B→C | 1 | A→B, D→B | 2 | 3 |
| C | C→A, C→C, C→E | 3 | B→C, C→C, D→C | 3 | 6 |
| D | D→B, D→C | 2 | A→D | 1 | 3 |
| E | — | 0 | C→E | 1 | 1 |
| Σ | 8 | 8 | 16 |
Σd⁺ = Σd⁻ = 8 = |E| ✓. Cross-check against the matrix: row sums are 2, 1, 3, 2, 0 and column sums are 1, 2, 3, 1, 1 — matching the table.
4. degree(C) = 6. In-degree 3 (B→C, C→C, D→C) plus out-degree 3 (C→A, C→C, C→E). The edge people get wrong is the self-loop C→C: it is one edge, but it leaves C and it arrives at C, so it adds 1 to the out-degree and 1 to the in-degree and therefore 2 to the total. Count it once and you get 5; ignore it entirely and you get 4. In the matrix it is the single diagonal cell A[C][C], and it is counted once by row C and once by column C, which is the same two contributions seen from the other side.
5. C→E: origin (source, predecessor, tail of the arrow) is C; destination (target, successor, head of the arrow) is E. The ordered pair is (C, E) — first coordinate is the origin, and the arrowhead is the destination. Do not read it off the positions of C and E in a drawing.
Exercise 3
The reverse graph. You have a directed graph as successor lists. You now need, for every vertex, the list of its predecessors — the vertices that point at it.
- Why can you not just read this off the successor lists directly, and what would the naive per-query approach cost?
- Write a C++ function that builds all the predecessor lists in one pass, and state its complexity.
- How would you get the predecessors of a single vertex out of an adjacency matrix instead, and what does that cost?
- Name two situations where you genuinely need the reverse graph.
Solution
1. Why it is not free. An adjacency list is indexed by source. The entry recording u → v lives in u's list and nowhere else, so vertex v has no local record of who points at it. To answer "who are v's predecessors?" from the lists alone you must scan every list looking for v — O(V + E) per query. Answer that question for all V vertices one at a time and you have done O(V·(V+E)) work to compute something that one pass can give you.
This is the exact asymmetry of the representation: successors are O(deg) to enumerate, predecessors are a full scan. The matrix has the mirror property — a column is as cheap as a row.
2. One pass. Walk every edge once and file it under its destination instead of its source.
#include <iostream>
#include <vector>
// Given the successor lists of a directed graph, build the predecessor lists
// (the reverse graph) in O(V + E).
std::vector<std::vector<int>> reverseGraph(const std::vector<std::vector<int>>& succ) {
std::vector<std::vector<int>> pred(succ.size());
for (std::size_t u = 0; u < succ.size(); ++u)
for (int v : succ[u])
pred[v].push_back(static_cast<int>(u)); // edge u->v seen from v
return pred;
}
int main() {
// 0->1 0->2 1->2 2->2 2->3 3->1 3->4 4->0
std::vector<std::vector<int>> succ = {{1, 2}, {2}, {2, 3}, {1, 4}, {0}};
std::vector<std::vector<int>> pred = reverseGraph(succ);
for (std::size_t v = 0; v < pred.size(); ++v) {
std::cout << " predecessors of " << v << ": ";
if (pred[v].empty()) std::cout << "-";
for (std::size_t k = 0; k < pred[v].size(); ++k)
std::cout << pred[v][k] << (k + 1 < pred[v].size() ? ", " : "");
std::cout << " in-degree = " << pred[v].size() << "\n";
}
}Output:
predecessors of 0: 4 in-degree = 1
predecessors of 1: 0, 3 in-degree = 2
predecessors of 2: 0, 1, 2 in-degree = 3
predecessors of 3: 2 in-degree = 1
predecessors of 4: 3 in-degree = 1Complexity: the outer loop runs V times and the inner body runs once per edge, so O(V + E) time and O(V + E) space — the reverse graph is the same size as the original. Two details: because the outer loop visits sources in increasing order, each predecessor list comes out sorted ascending with no extra work; and pred[v].size() is exactly d⁻(v), so this function is also the cheapest way to get all the in-degrees at once. The self-loop on 2 correctly makes 2 its own predecessor.
3. From a matrix. The predecessors of v are the rows with a 1 in column v: loop i from 0 to V−1 and collect every i with A[i][v] != 0. That is O(V) for one vertex and O(V²) for all of them — but O(V²) is also what it costs to scan the matrix at all, so on a matrix the reverse direction is no more expensive than the forward one. Transposing the matrix (A'[i][j] = A[j][i]) gives you the reverse graph outright.
4. When you need it. Two clear cases:
- In-degrees for topological sorting. Kahn's algorithm starts from every vertex with in-degree 0 and decrements in-degrees as it removes vertices. You need the counts, and one reverse pass produces all of them in
O(V + E). - Answering "what depends on this?" A build system, package manager or spreadsheet stores edges as X depends on Y. To find everything affected by a change to
Yyou have to walk the arrows backwards, which means traversing the reverse graph. The same shape appears in "who links to this page", "who follows this account", and in strongly-connected-components algorithms such as Kosaraju's, which runs a second DFS on the reversed graph.
Published and updated 2026-08-09 · DUOCODE TECHNOLOGY