DSA Graphs
Graph Implementation
The query hits: is user 10⁷ connected to user 42? Your machine does not store circles and lines. It stores arrays, maps, and bits. The representation chooses which questions are cheap.
Pick an , , , or from the operations you run, then defend if edges are bidirectional.
Decision panel
Hot operations
Traverse neighbors? Test (u,v)? Mutate often?
Density
Sparse → list/CSR; dense pair tests → matrix.
Direction contract
Undirected needs two entries; directed does not.
Isolates
Allocate all V buckets before inserting edges.
What actually happens
Naive idea: always use a V×V matrix because edge tests are O(1). At V = 10⁵ that is impossible; at sparse social graphs most cells are empty forever.
Better idea: store only existing edges in lists (or CSR for static scans). Pay O(deg(u)) to test a specific neighbor unless you add a hash side index.
The improved design still fails on half-updated undirected edges, on dropping isolates when building from an edge list, and on hash iteration order that makes “the” BFS order nondeterministic.
Invariant
Every vertex in V has a storage slot (even if empty). For undirected graphs, (u,v) present implies (v,u) present with the same weight policy. Algorithms may assume that symmetry; a half-edge silently lies.
How to choose a representation
1. Traversal-heavy and sparse? Adjacency list (or CSR if static).
2. Constant-time arbitrary edge tests on dense V? Matrix.
3. Sorting edges by weight? Keep an edge list (Kruskal) and optionally build lists later.
Quantitative check
List space ≈ O(V + E); matrix ≈ O(V²). For V = 10⁴, E = 3×10⁴, lists win by orders of magnitude. Neighbor scan on a matrix still touches V entries per vertex, so BFS becomes O(V²) even when E is small.
1. Adjacency Matrix
A 2D array (grid) of size V x V. If matrix[i][j] == 1, there is an edge from i to j.
2. Adjacency List
A collection of lists (or a dictionary). Each vertex stores a list of its neighbors.
Representation explorer
The graph below is identical in both views. Toggle directed/undirected to see why a symmetric matrix and two adjacency entries are implementation consequences, not extra logical edges.
Interactive invariant lab
Graph state, not just graph shape
Switch semantics and algorithms, then step through the frontier, discovery state, and edge-level proof obligation.
Semantic contract
Undirected weighted graph
Each logical edge is listed in both endpoint buckets, but still counts once in |E|.
Invariant
For every logical edge {u, v}, v appears in adj[u] and u appears in adj[v].
Same graph, different physical layout
Weights are stored beside the destination; ∞ means no edge.
Choose by operations, not habit
| Representation | Space | Edge query | Scan neighbors | Best fit |
|---|---|---|---|---|
| Adjacency list (array) | O(V + E) | O(deg(u)) | O(deg(u)) | Sparse traversal-heavy graphs |
| Adjacency sets/maps | O(V + E) | Expected O(1) | O(deg(u)) | Frequent insert/delete/contains |
| Adjacency matrix | O(V²) | O(1) | O(V) | Dense graph or constant-time pair queries |
| Edge list | O(E) | O(E) | O(E) | Bellman–Ford, Kruskal, interchange |
| CSR | O(V + E) | O(deg(u)) | O(deg(u)) | Static, cache-friendly massive graphs |
A hash-backed neighbor map changes the edge-query bound, but adds hashing, object, and resizing overhead. State whether your complexity assumes a list, set, matrix, or compressed layout.
Weighted edges & lookup nuance
Storing weights
Adjacency lists usually store (neighbor, weight) pairs. Matrices store w[u][v] (or ∞ if absent). Multi-edges can be merged by keeping the minimum weight or kept explicitly depending on the problem.
“Is (u,v) an edge?”
Matrix: O(1). List: scan u's neighbors-O(deg(u)) without a side hash from neighbor → weight. For static graphs that query arbitrary edges often, an edge hash map or matrix slice can help.
CSR / COO (advanced)
For very large sparse graphs (scientific computing, PageRank prototypes), compressed sparse row (CSR) packs the adjacency list into parallel-friendly arrays with prefix sums over degrees.
Build safely from an edge list
Allocate all n buckets first so isolated vertices survive. Validate endpoints at the boundary, and add the reverse entry only when the input contract is undirected.
Python
def build_graph(n, edges, directed=False):
adj = [[] for _ in range(n)] # keeps isolated vertices
for u, v, weight in edges:
if not (0 <= u < n and 0 <= v < n):
raise ValueError("edge endpoint out of range")
adj[u].append((v, weight))
if not directed:
adj[v].append((u, weight))
return adjJava
import java.util.*;
final class GraphBuilder {
record Edge(int to, long weight) {}
static List<List<Edge>> buildGraph(
int n, long[][] edges, boolean directed) {
List<List<Edge>> adj = new ArrayList<>(n);
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (long[] edge : edges) {
int u = Math.toIntExact(edge[0]);
int v = Math.toIntExact(edge[1]);
long weight = edge[2];
if (u < 0 || u >= n || v < 0 || v >= n) {
throw new IllegalArgumentException("edge endpoint out of range");
}
adj.get(u).add(new Edge(v, weight));
if (!directed) adj.get(v).add(new Edge(u, weight));
}
return adj;
}
}Implementation (adjacency list)
We usually prefer the Adjacency List because most real-world graphs are sparse (not everything is connected to everything). Here's how to implement a Graph class in Python and Java using a dictionary/map.
Python
class Graph:
def __init__(self):
# Dictionary to store the graph: {vertex: [neighbors]}
self.adj_list = {}
def add_vertex(self, vertex):
if vertex not in self.adj_list:
self.adj_list[vertex] = []
def add_edge(self, v1, v2):
# Add v1 and v2 if they don't exist
if v1 not in self.adj_list:
self.add_vertex(v1)
if v2 not in self.adj_list:
self.add_vertex(v2)
# Add connection (Undirected Graph)
self.adj_list[v1].append(v2)
self.adj_list[v2].append(v1)
def print_graph(self):
for vertex in self.adj_list:
print(f"{vertex} -> {self.adj_list[vertex]}")
# Usage
my_graph = Graph()
my_graph.add_edge('A', 'B')
my_graph.add_edge('B', 'C')
my_graph.add_edge('C', 'A')
my_graph.print_graph()
# Output:
# A -> ['B', 'C']
# B -> ['A', 'C']
# C -> ['B', 'A']Java
import java.util.*;
class Graph {
// Map to store graph: Vertex -> List of Neighbors
private Map<String, List<String>> adjList;
public Graph() {
adjList = new HashMap<>();
}
public void addVertex(String vertex) {
adjList.putIfAbsent(vertex, new ArrayList<>());
}
public void addEdge(String v1, String v2) {
addVertex(v1);
addVertex(v2);
// Add connection (Undirected Graph)
adjList.get(v1).add(v2);
adjList.get(v2).add(v1);
}
public void printGraph() {
for (String vertex : adjList.keySet()) {
System.out.println(vertex + " -> " + adjList.get(vertex));
}
}
public static void main(String[] args) {
Graph myGraph = new Graph();
myGraph.addEdge("A", "B");
myGraph.addEdge("B", "C");
myGraph.addEdge("C", "A");
myGraph.printGraph();
// Output:
// A -> [B, C]
// B -> [A, C]
// C -> [B, A]
}
}Mutation invariants
Undirected symmetry
Adding or deleting {u,v} must update both buckets atomically. A half-edge silently corrupts degree counts, traversals, and cycle detection.
Duplicate policy
A list permits parallel edges; a set deduplicates them; a neighbor→weight map overwrites unless you combine weights. Choose intentionally.
Vertex deletion
Removing a vertex also requires removing every incident edge. An incoming-edge index can avoid scanning every bucket in a directed graph.
Deterministic order
Hash iteration order should not define externally visible traversal output. Sort once for static graphs or use ordered collections when reproducibility matters.
CSR: why static graphs can be faster
Compressed sparse row stores one offsets array of length V + 1 and one contiguous neighbors array of length E (or 2E for an undirected graph). Neighbors of u occupy neighbors[offsets[u]..offsets[u+1]). It avoids one object per list/edge and improves cache locality.
Implementation review checklist
- ✓ Are vertex IDs dense, sparse, or strings, and is remapping required?
- ✓ Are self-loops and parallel edges legal, ignored, or rejected?
- ✓ Can weights exceed
int, and what represents “no edge” without colliding with a real weight? - ✓ Does
Ecount logical undirected edges once even though storage contains two entries? - ✓ Do empty buckets preserve isolated vertices and disconnected components?
- ✓ Is the graph immutable during algorithms, or do readers need locking/versioned snapshots?
Simple rule of thumb
Use an adjacency list for most sparse graphs (social, web, road networks).
Use an adjacency matrix when the graph is dense or you need extremely fast arbitrary edge lookups.
Exercises
- Build from edges [(0,1),(1,2)] with n = 5. How many empty adjacency buckets must exist, and what goes wrong if you only create keys that appear in E?
- An undirected add_edge updates only adj[u]. Which algorithms silently misbehave, and how would a degree-sum check catch it?
- V = 5000, E ≈ V²/2. Would you still prefer lists for BFS? Estimate neighbor-scan work for list vs matrix.
Primary sources
- Cormen et al., Introduction to Algorithms (CLRS): adjacency-list and adjacency-matrix representations.
- Sedgewick & Wayne: Graph ADT implementations in practice.
- Scientific computing notes on CSR/CSC sparse formats (e.g. SciPy sparse documentation).
Takeaway
Representation is an operations budget. Keep V complete, keep undirected edges symmetric, and state whether complexity assumes lists, sets, or a matrix.