An adjacency list stores, for each vertex u, a container of its outgoing neighbors Adj[u] = {v : (u,v) ∈ E} (optionally with edge attributes like weights or ids). For undirected graphs, each undirected edge {u,v} is represented twice: once in Adj[u] and once in Adj[v].
Invariants
Each directed edge (u,v) appears exactly once in Adj[u] (twice overall for undirected graphs).
The chosen graph policy is consistent:
Simple graph: no parallel edges; self-loops optional/forbidden by policy.
Multigraph: parallel edges allowed (distinguished by ids or payloads).
If maintaining inDegree/outDegree arrays, updates mirror insertions/deletions in the lists.
Operations
Typical surface API (names vary by codebase):
addVertex() — allocate a new, initially empty neighbor container.
addEdge(u, v, w?) — insert (v, w?) into Adj[u] (and (u, w?) into Adj[v] if undirected).
removeEdge(u, v) — delete one (u,v) occurrence (and (v,u) if undirected); in multigraphs, delete by edge-id.
neighbors(u) — return an iterator over Adj[u].
Minimal sketch:
function addEdge(Adj, u, v, directed = true, simple = true): if simple and contains(Adj[u], v): return push_back(Adj[u], v) if not directed and not (simple and contains(Adj[v], u)): push_back(Adj[v], u)
Choose containers by your access pattern
Append-heavy, occasional deletion: vector<int> per bucket with swap-pop deletions.
Frequent membership tests: unordered_set<int> per bucket (expected O(1) test).
Need order (by id): std::set<int> or std::vector<int> kept sorted (binary search).
Insert & Remove (vector buckets)
// insert edge u->v if absentauto& L = adj[u];if (std::find(L.begin(), L.end(), v) == L.end()) L.push_back(v);// remove edge u->v using swap-pop (order not preserved)auto it = std::find(L.begin(), L.end(), v);if (it != L.end()) { std::swap(*it, L.back()); L.pop_back(); }
Maintain degrees in O(1)
out_deg[u]++; // on successful insert u->vin_deg[v]++;// ...and decrement on deletion
Complexity
Space:O(n + m) overall (headers for n vertices plus one record per directed edge; two per undirected edge).
Edge existence test hasEdge(u,v):O(deg(u)) with vector/list; expected O(1) with a hash-set bucket; O(log deg(u)) with a tree-set bucket.
Neighbor iteration:O(deg(u)) to visit all of u’s neighbors.
Compared with an adjacency matrix (O(n²) space, O(1) membership, O(n) neighbor scan), adjacency lists dominate sparse workloads.
Typical costs at a glance
Space: O(n + m)
hasEdge(u,v): O(deg(u)) for vector/list; expected O(1) for hash-set
Represent edges as tuples (neighbor, weight[, id]) if you need payloads.
Pitfalls
Duplicate edges in simple graphs inflate degrees and double-count relaxations—deduplicate on insert or via a cleanup pass.
Undirected symmetry must be maintained on insert/delete, or degrees and traversals become inconsistent.
Self-loops & parallel edges: decide policy up front; loops affect degree counts and some heuristics.
Iterator invalidation: removing from a vector while iterating can invalidate indices/iterators; prefer two-phase delete or stable containers.
Mixed vertex id spaces: if vertex ids are sparse/non-contiguous, prefer maps over raw arrays to avoid large unused buckets.
Concurrency
Multi-writer updates to shared buckets require synchronization (per-vertex locks or immutable snapshots). Traversals over a mutating graph can observe torn buckets unless you guard access.
When to use
Choose adjacency lists for sparse graphs and algorithms that iterate neighbors (BFS/DFS/Dijkstra/Prim). They minimize memory, keep work proportional to present edges, and provide flexible bucket choices (locality vs membership speed) for your workload.