Graphs
Think of it like this
A graph is a map of relationships. Cities connected by roads. Users connected by friendships. Modules connected by imports. Tasks connected by dependencies.
Unlike trees (which have a strict parent-child hierarchy), graphs have no root and edges can go in any direction — even forming cycles.
Core Vocabulary
Vertices (V): The nodes — cities, users, modules, tasks
Edges (E): The connections — roads, friendships, imports
Undirected: A — B (road goes both ways)
Directed: A → B (one-way street; also called "digraph")
Weighted: A —5→ B (road has a distance/cost)
Cyclic: A → B → C → A (you can get back to where you started)
Acyclic: A → B → C (no cycles)
DAG: Directed Acyclic Graph (task dependencies, package imports)
Connected: Every vertex is reachable from every other (undirected)Representations
Choose based on your graph's density:
// 1. Adjacency List — best for sparse graphs (most graphs in interviews)
// Space: O(V + E) Edge check: O(degree)
const graph = {
A: ['B', 'C'],
B: ['A', 'D', 'E'],
C: ['A', 'F'],
D: ['B'],
E: ['B', 'F'],
F: ['C', 'E'],
};
// For weighted graphs:
const weighted = {
A: [['B', 4], ['C', 2]], // [neighbor, weight]
B: [['A', 4], ['D', 3]],
C: [['A', 2], ['D', 1]],
D: [['B', 3], ['C', 1]],
};
// 2. Adjacency Matrix — best for dense graphs
// Space: O(V²) Edge check: O(1)
// matrix[i][j] = 1 (or weight) means edge from i to j
const matrix = [
//A B C D
[ 0, 1, 1, 0 ], // A
[ 1, 0, 0, 1 ], // B
[ 1, 0, 0, 1 ], // C
[ 0, 1, 1, 0 ], // D
];
// 3. Edge List — simple but slow for neighbor lookup
// Space: O(E) Used in Kruskal's MST
const edges = [['A','B'], ['A','C'], ['B','D'], ['C','D']];BFS vs DFS — When to Use Which
| Property | BFS (Queue) | DFS (Stack/Recursion) |
|---|---|---|
| Data structure | Queue | Stack or call stack |
| Shortest path (unweighted)? | Yes | No |
| Detects cycles? | Yes | Yes (easier) |
| Topological sort? | Yes (Kahn's) | Yes (DFS + reverse postorder) |
| Memory (sparse graph) | O(width) | O(height) |
| Best for | Shortest path, level-by-level, multi-source | Connected components, cycle detection, topo sort |
BFS — Breadth-First Search
Explores level by level. Guarantees shortest path in unweighted graphs.
function bfs(graph, start) {
const visited = new Set([start]);
const queue = [start];
const dist = { [start]: 0 };
const parent = { [start]: null };
while (queue.length) {
const node = queue.shift();
for (const neighbor of (graph[node] ?? [])) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
dist[neighbor] = dist[node] + 1;
parent[neighbor] = node;
queue.push(neighbor);
}
}
}
return { dist, parent };
}
// Reconstruct shortest path from start to end
function getPath(parent, end) {
const path = [];
for (let node = end; node !== null; node = parent[node]) {
path.unshift(node);
}
return path;
}DFS — Depth-First Search
Dives as deep as possible before backtracking. Essential for cycle detection and topological sort.
// Recursive DFS
function dfs(graph, node, visited = new Set()) {
if (visited.has(node)) return;
visited.add(node);
console.log(node); // process node
for (const neighbor of (graph[node] ?? [])) {
dfs(graph, neighbor, visited);
}
}
// Iterative DFS (avoids call stack overflow on large graphs)
function dfsIterative(graph, start) {
const visited = new Set();
const stack = [start];
const order = [];
while (stack.length) {
const node = stack.pop();
if (visited.has(node)) continue;
visited.add(node);
order.push(node);
for (const neighbor of (graph[node] ?? [])) {
if (!visited.has(neighbor)) stack.push(neighbor);
}
}
return order;
}Graph BFS (Level Order Traversal)
Queue: [A]. Visit A first.
Cycle Detection
// Undirected graph — DFS with parent tracking
function hasCycleUndirected(graph) {
const visited = new Set();
function dfs(node, parent) {
visited.add(node);
for (const neighbor of (graph[node] ?? [])) {
if (!visited.has(neighbor)) {
if (dfs(neighbor, node)) return true;
} else if (neighbor !== parent) {
return true; // back edge to non-parent = cycle
}
}
return false;
}
for (const node in graph) {
if (!visited.has(node) && dfs(node, null)) return true;
}
return false;
}
// Directed graph — DFS with "recursion stack" tracking
function hasCycleDirected(graph) {
const visited = new Set();
const inStack = new Set(); // nodes in current DFS path
function dfs(node) {
visited.add(node);
inStack.add(node);
for (const neighbor of (graph[node] ?? [])) {
if (!visited.has(neighbor) && dfs(neighbor)) return true;
if (inStack.has(neighbor)) return true; // back edge = cycle
}
inStack.delete(node);
return false;
}
for (const node in graph) {
if (!visited.has(node) && dfs(node)) return true;
}
return false;
}Topological Sort (Kahn's BFS Algorithm)
Orders nodes so every directed edge points from earlier to later in the order. Used for build systems, course prerequisites, package install order.
function topoSort(numNodes, edges) {
// Build adjacency list and in-degree count
const adj = Array.from({ length: numNodes }, () => []);
const inDegree = Array(numNodes).fill(0);
for (const [u, v] of edges) {
adj[u].push(v);
inDegree[v]++;
}
// Start with all nodes that have no prerequisites
const queue = [];
for (let i = 0; i < numNodes; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const order = [];
while (queue.length) {
const node = queue.shift();
order.push(node);
for (const neighbor of adj[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor); // unlocked!
}
}
// If order has all nodes → valid sort. Otherwise → cycle exists.
return order.length === numNodes ? order : [];
}
// Example: Course Schedule
// 0→1 (take 0 before 1), 0→2, 1→3, 2→3
// inDegree: [0, 1, 1, 2]
// Start: queue=[0]
// Process 0: unlock 1,2 → queue=[1,2], order=[0]
// Process 1: unlock 3 → queue=[2,3], order=[0,1] (inDegree[3]=1)
// Process 2: unlock 3 → queue=[3], order=[0,1,2] (inDegree[3]=0)
// Process 3: queue=[], order=[0,1,2,3] ← valid topological orderDijkstra's Algorithm (Shortest Path with Weights)
Find the shortest path from one source to all other nodes. Works only with non-negative weights.
function dijkstra(graph, start) {
const dist = {};
const visited = new Set();
for (const node in graph) dist[node] = Infinity;
dist[start] = 0;
// Priority queue (min-heap by distance)
// Using a sorted array here for clarity; use a real heap for O((V+E)logV)
const pq = [[0, start]]; // [distance, node]
while (pq.length) {
pq.sort((a, b) => a[0] - b[0]); // sort by distance
const [cost, node] = pq.shift();
if (visited.has(node)) continue;
visited.add(node);
for (const [neighbor, weight] of (graph[node] ?? [])) {
const newDist = dist[node] + weight;
if (newDist < dist[neighbor]) {
dist[neighbor] = newDist;
pq.push([newDist, neighbor]);
}
}
}
return dist;
}Algorithm Complexity Reference
| Algorithm | Time | Space | Use Case |
|---|---|---|---|
| BFS | O(V + E) | O(V) | Shortest path (unweighted) |
| DFS | O(V + E) | O(V) | Cycle detect, topo sort, connected components |
| Dijkstra | O((V + E) log V) | O(V) | Shortest path (non-negative weights) |
| Bellman-Ford | O(V × E) | O(V) | Shortest path with negative weights |
| Floyd-Warshall | O(V³) | O(V²) | All-pairs shortest path |
| Kruskal / Prim | O(E log E) | O(V) | Minimum Spanning Tree |
| Tarjan's SCC | O(V + E) | O(V) | Strongly Connected Components |
Real-World Frontend Application
- Webpack/Vite dependency graph: Every
importstatement is a directed edge. The bundler runs DFS/BFS from entry points to discover all modules. Circular imports = cycles in the graph - npm dependency resolution:
npm installbuilds a graph of package dependencies, runs topological sort to determine install order - State machines (XState): Every state is a vertex; every event/transition is a directed edge. The machine is literally a directed graph
- Routing in SPAs: A navigation graph where pages are vertices and links are edges; "404" = no path in the graph
- GraphQL: The query language is literally named after graphs — a type system where types reference each other forms a graph
Common Mistakes
| Mistake | What Goes Wrong | Fix |
|---|---|---|
| Not marking visited before enqueuing (BFS) | Same node processed multiple times → infinite loop | Mark visited when enqueuing, not when dequeuing |
| Using BFS for DFS problem | Level-order when you need depth-first | BFS = queue; DFS = stack/recursion |
| Ignoring disconnected components | DFS from one start misses isolated subgraphs | Outer loop over all nodes: for node in graph |
| Using Dijkstra with negative weights | Incorrect results (negative edges break the greedy assumption) | Use Bellman-Ford for negative weights |
Key Problems to Solve
| # | Problem | Pattern | Difficulty |
|---|---|---|---|
| 1 | Number of Islands | DFS/BFS on grid | Medium |
| 2 | Clone Graph | DFS + hash map | Medium |
| 3 | Course Schedule | Topological sort | Medium |
| 4 | Pacific Atlantic Water Flow | Multi-source BFS | Medium |
| 5 | Network Delay Time | Dijkstra | Medium |
| 6 | Redundant Connection | Union-Find | Medium |
| 7 | Word Ladder | BFS over word states | Hard |
| 8 | Alien Dictionary | Topological sort on chars | Hard |
| 9 | Critical Connections (Bridges) | Tarjan's DFS | Hard |
| 10 | Cheapest Flights Within K Stops | Bellman-Ford variant | Medium |
| 11 | Minimum Cost to Connect All Points | Prim's / Kruskal MST | Medium |