// 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.

TermWhat 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 graphEdges have no direction: (u, v) and (v, u) are the same edge and it can be traversed both ways.
Weighted graphEvery edge carries a numeric value — cost, distance, time, capacity.
Unweighted graphEdges record only that a connection exists.
AdjacentTwo vertices joined directly by an edge. In a digraph this word is ambiguous — see the section on direction below.
IncidentThe 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).
PathA sequence of vertices in which each consecutive pair is joined by an edge.
Simple pathA path in which no vertex repeats (except possibly the first and last).
CycleA path that starts and ends at the same vertex.
Self-loopA single edge from a vertex to itself, v → v.
Connected graphUndirected: a path exists between every pair of vertices.
Strongly connected digraphDirected: every vertex can reach every other vertex following the arrows.
Weakly connected digraphBecomes connected only once you ignore the directions.
Complete graphEvery vertex is directly joined to every other. Undirected, that is n(n−1)/2 edges.
SubgraphA graph H with V(H) ⊆ V(G) and E(H) ⊆ E(G).
Sparse graphE is far smaller than — most possible edges are absent.
Dense graphE is close to — most possible edges are present.
Spanning treeA cycle-free subgraph touching every vertex, using exactly V − 1 edges.
DAGDirected 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, G is 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, B is 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, uThe head of the arrow, v
origindestination
sourcetarget
predecessor of vsuccessor of u
the vertex the edge leavesthe vertex the edge arrives at
counts towards d⁺(u), out-degreecounts towards d⁻(v), in-degree
written first in the pair (u, v)written second in the pair (u, v)
fills matrix cell row ufills matrix cell column v
stored in u's adjacency listnot 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]
unweighted1 if the edge exists, 0 if not
weightedthe 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
undirectedsymmetric: an edge fills both A[i][j] and A[j][i]
directednot symmetric: edge (u, v) fills only A[u][v]
self-loop on vthe 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:

ABCDEFG
A0110000
B1001100
C1000110
D0100001
E0110001
F0010001
G0001110

Three checks you can run on any undirected matrix in about five seconds, and should:

  1. 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.
  2. The number of 1s is exactly 2|E|. Here 18, for nine edges. Every undirected edge is recorded twice.
  3. 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: 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, F

The 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: v appears 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, E and B -> E, D, A describe 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
cpp
#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 matrixAdjacency list
SpaceO(V²)O(V + E)
Is there an edge (i, j)?O(1)O(deg(i)), worst case O(V)
List all neighbours of vO(V) — scan the whole rowO(deg(v))
Add an edgeO(1)O(1) at the head of the list
Remove an edgeO(1)O(deg) — you have to find it first
Iterate over every edgeO(V²)O(V + E)
Add a vertexO(V²) — reallocate the gridO(1) — push one empty list
Best fordense, near-complete graphs; heavy edge-existence queryingsparse graphs, E ≪ V²; traversal-heavy work
Weaknesswastes memory on a sparse graph — mostly zerospointer/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, 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 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 , 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, 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)
cpp
#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 have v in the first slot — (v, _).
  • In-degree d⁻(v): count how many pairs have v in the second slot — (_, v).
  • Total degree of v in a digraph is d⁺(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:

CheckUndirectedDirected
Matrix symmetric?must beusually not
Non-sentinel cells2 × (number of edges)one per edge
Total adjacency-list entries2 × (number of edges)one per edge
Non-sentinel cells in row ideg(i)d⁺(i)
Non-sentinel cells in column jdeg(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
cpp
#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 u is adjacent to v, and v is adjacent from u — the preposition carries the direction, and "the vertices adjacent to v" therefore means v'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 u are the ones u points 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:

  1. 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.
  2. Watch for a parenthetical. A question that says "adjacent (successor) vertices" has told you which one it means, and that overrides everything.
  3. 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

  1. Filling one matrix cell for an undirected edge. A[i][j] = 1 without A[j][i] = 1 silently gives you a directed graph. The tell is that the matrix is not symmetric; check for symmetry every single time.
  2. 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 at 2|E|, and any shortest-path answer is wrong in a way that still looks plausible.
  3. 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.
  4. Dropping the self-loop out of a vertex's own list. 2 → 2 means 2 appears in 2's successor list. It looks redundant, so it gets dropped, and then the degree count is wrong by two.
  5. Forgetting the V in O(V + E). The adjacency list needs an array of V heads whether or not the vertices have any edges, so an edgeless graph on a million vertices still costs a million pointers. The space is O(V + E), never O(E).
  6. Using 0 as 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.
  7. 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

  1. 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.
  2. 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 is X.
  3. 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.
  4. 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.
  5. Not running Σ d⁺ = Σ d⁻ = |E|. Ten seconds, and it catches missed edges, doubled edges and reversed arrows. The undirected equivalent is Σ deg = 2|E|.
  6. Treating "adjacent to" as self-evident in a digraph. It is not; see the previous section. Say successors or predecessors.
  7. 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. When E ≪ 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

  1. Write the 6×6 adjacency matrix.
  2. Write the adjacency list, neighbours in ascending order.
  3. Give deg(v) for every vertex, and verify the handshake lemma.
  4. State how many bytes each representation would need if you stored the matrix as int A[6][6] and the list as one int per entry plus one pointer per vertex (take int = 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.

012345
0011000
1100110
2100010
3010011
4011101
5000110

2. Adjacency list.

0 -> 1, 2
1 -> 0, 3, 4
2 -> 0, 4
3 -> 1, 4, 5
4 -> 1, 2, 3, 5
5 -> 3, 4

Sixteen 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.

v012345
deg(v)232342

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 , 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

  1. Write the adjacency matrix with row = from, column = to.
  2. Write the adjacency list of successors, ascending, using a dash for a vertex with none.
  3. Tabulate d⁺, d⁻ and total degree for every vertex, and run the sum check.
  4. What is degree(C), and which single edge is responsible for the part of it that people get wrong?
  5. 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.

ABCDE
A01010
B00100
C10101
D01100
E00000

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.

vout-edgesd⁺in-edgesd⁻degree
AA→B, A→D2C→A13
BB→C1A→B, D→B23
CC→A, C→C, C→E3B→C, C→C, D→C36
DD→B, D→C2A→D13
E0C→E11
Σ8816

Σ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.

  1. Why can you not just read this off the successor lists directly, and what would the naive per-query approach cost?
  2. Write a C++ function that builds all the predecessor lists in one pass, and state its complexity.
  3. How would you get the predecessors of a single vertex out of an adjacency matrix instead, and what does that cost?
  4. 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 vO(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.

cpp
#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 = 1

Complexity: 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 Y you 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