Graph Fundamentals — The Language of Connections

Before you write a single line of graph algorithm code, you need the vocabulary. Not for its own sake, but because the representation you choose determines your algorithm’s time and space complexity, and the wrong choice for the wrong problem is the kind of mistake that costs you in an interview before you’ve even started solving it.

Graphs are described by their vertices (things) and edges (relationships between things). Every problem that involves connectivity, dependencies, distances, or reachability is a graph problem, whether the word “graph” appears in the problem statement or not.


Core Definitions

Vertex (node): a fundamental unit. Could represent a city, a course, a character, a cell in a grid.

Edge: a connection between two vertices. Could be a road, a prerequisite relationship, an adjacency in a grid.

Directed graph (digraph): edges have direction. Edge u→v means you can go from u to v, but not necessarily v to u. Example: course prerequisites, social media “follows.”

Undirected graph: edges have no direction. If u is connected to v, then v is connected to u. Example: friendships, road networks (ignoring one-ways).

Weighted graph: each edge carries a numeric weight (cost, distance, capacity). Example: road with distance, network link with bandwidth.

Unweighted graph: edges are just present or absent, no weight. For shortest path purposes, every edge has implicit weight 1.

Cyclic graph: contains at least one cycle (a path that starts and ends at the same vertex).

Acyclic graph: contains no cycles. A directed acyclic graph is a DAG — the structure that enables topological sort.

Connected graph (undirected): every vertex is reachable from every other vertex.

Strongly connected (directed): every vertex is reachable from every other vertex following edge directions.

Degree: number of edges incident to a vertex. In directed graphs: in-degree (edges coming in) and out-degree (edges going out).


Graph Representations

Three representations exist. Each has distinct trade-offs. The choice should follow from the problem’s structure, not habit.


Adjacency Matrix

A 2D boolean (or weight) matrix adj[V][V] where adj[u][v] = true (or the edge weight) if edge u→v exists.

int V = 5;
int[][] adj = new int[V][V];   // adj[u][v] = weight; 0 means no edge

// Add edge u→v with weight w:
adj[u][v] = w;
// For undirected, also: adj[v][u] = w;

Operation

Cost

Check if edge (u,v) exists

O(1)

Iterate all neighbors of u

O(V)

Space

O(V²)

When to use: dense graphs (E ≈ V²), when you need O(1) edge existence queries, Floyd-Warshall.

When NOT to use: sparse graphs (most real-world graphs). If V = 10,000 and E = 50,000, the matrix wastes 10⁸ cells for ~50,000 actual edges.


Adjacency List

An array (or map) of lists: adj[u] contains all vertices v where edge u→v exists. For weighted graphs, store pairs (v, weight).

int V = 5;
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < V; i++) adj.add(new ArrayList<>());

// Add edge u→v:
adj.get(u).add(v);
// For weighted: List<List<int[]>> where int[] = {v, weight}
// For undirected: also adj.get(v).add(u);

Operation

Cost

Check if edge (u,v) exists

O(degree(u))

Iterate all neighbors of u

O(degree(u))

Space

O(V + E)

When to use: sparse graphs, BFS, DFS, Dijkstra, topological sort — basically everything on LeetCode.


Edge List

A list of all edges, each represented as a tuple (u, v) or (u, v, weight).

int[][] edges = {{0,1,4}, {0,2,1}, {1,3,2}, ...};  // [from, to, weight]

Operation

Cost

Check if edge (u,v) exists

O(E)

Iterate all edges

O(E)

Space

O(E)

When to use: algorithms that process all edges in sorted order (Kruskal’s MST). Not useful for traversal.


Decision Table: Which Representation?

Situation

Use

Graph is sparse (E << V²)

Adjacency list

Graph is dense (E ≈ V²)

Adjacency matrix

Need O(1) edge lookup

Adjacency matrix

Need to sort/process all edges

Edge list

BFS, DFS, Dijkstra, topological sort

Adjacency list

Floyd-Warshall

Adjacency matrix

Kruskal’s MST

Edge list (sorted)

For LeetCode: default to adjacency list. You will rarely be wrong.


Traversal Invariants: The Visited Set

Every graph traversal maintains a visited set (or array). Without it, cycles cause infinite loops.

boolean[] visited = new boolean[V];

// In BFS or DFS:
if (!visited[neighbor]) {
    visited[neighbor] = true;
    // enqueue or recurse
}

What most people get wrong: marking a node visited at the wrong time. In BFS, mark the node visited when you enqueue it, not when you dequeue and process it. If you mark on dequeue, the same node can be enqueued multiple times before it’s processed, leading to redundant work and potentially wrong results (e.g., non-shortest paths processed).

// CORRECT BFS visited marking:
queue.offer(start);
visited[start] = true;       // ← mark BEFORE processing, AT enqueue time

while (!queue.isEmpty()) {
    int node = queue.poll();  // process here
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) {
            visited[neighbor] = true;  // mark AT enqueue time
            queue.offer(neighbor);
        }
    }
}

Connected Components

In an undirected graph, a connected component is a maximal set of vertices all reachable from each other.

BFS/DFS from a single node only covers its connected component. To find all components:

int components = 0;
boolean[] visited = new boolean[V];

for (int i = 0; i < V; i++) {
    if (!visited[i]) {
        components++;
        bfs(i, visited, adj);   // or dfs(i, visited, adj)
    }
}

What most people get wrong: running BFS/DFS from only vertex 0 and concluding they’ve traversed the whole graph. If the graph has disconnected components, they’ve only seen one. Always loop over all vertices and trigger a new traversal for each unvisited one.


Building a Graph from a Problem

Most LeetCode graph problems don’t hand you an adjacency list. They give you edge pairs, a grid, or a set of relationships. Build the graph first.

From edge list (e.g., course prerequisites):

// prerequisites[i] = [ai, bi] means bi must be taken before ai
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < numCourses; i++) adj.add(new ArrayList<>());

for (int[] pre : prerequisites) {
    adj.get(pre[1]).add(pre[0]);  // bi → ai (bi must come before ai)
}

From a grid (e.g., number of islands):

// Grid cells are vertices. Edges connect adjacent cells.
// No explicit adjacency list needed — neighbors are computed on the fly:
int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
for (int[] d : dirs) {
    int nr = r + d[0], nc = c + d[1];
    if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
        // (nr, nc) is a neighbor of (r, c)
    }
}

The grid approach is an implicit graph: you never build an explicit adjacency list, you compute neighbors on the fly during traversal.


Practice Problems

#

Problem

Difficulty

Focus

1

LC 547 — Number of Provinces

Medium

BFS/DFS to count connected components

2

LC 200 — Number of Islands

Medium

Grid as implicit graph, connected components

3

LC 133 — Clone Graph

Medium

BFS with node mapping, graph construction

4

LC 207 — Course Schedule

Medium

Directed graph, cycle detection (preview of topo sort)

Problems 1 and 2 are the foundation problems — do not skip them. They establish the “loop over all vertices to find all components” pattern that you will use in every subsequent graph problem. Problem 4 is a forward bridge into topological sort.