Senior 4 min · March 06, 2026

Graph Interview Problems - Topological Sort Cycle Bug

A single undetected cycle in a topological sort OOM-killed our build server.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Graph problems appear in ~30% of FAANG coding rounds because real systems (Maps, Social Nets, Dispatch) are graphs under the hood.
  • Core patterns: BFS for shortest paths (unweighted), DFS for cycles/connectivity, Topological Sort for dependencies, Union-Find for dynamic connectivity.
  • Choosing the wrong traversal (DFS for shortest path) is the #1 mistake that costs an offer.
  • Performance trap: Adjacency List (O(V+E) space) beats Adjacency Matrix (O(V²) space) for sparse interview graphs.
  • Production insight: Forgetting to mark visited nodes causes infinite loops — same failure pattern in dependency resolution at scale.
Plain-English First

Imagine a city map. Every intersection is a dot, and every road connecting two intersections is a line. A graph is exactly that — dots (nodes) connected by lines (edges). Graph problems ask you things like: 'What's the shortest route from home to school?', 'Can I get from any city to any other city?', or 'Is there a road that, if it breaks, cuts two towns off from each other?' Every GPS app, social network, and package delivery system solves graph problems thousands of times per second.

Graph problems are the great filter of software engineering interviews. They appear in roughly 30% of FAANG-level coding rounds not because interviewers love theory, but because real systems — Google Maps, LinkedIn's friend-of-a-friend suggestions, Uber's dispatch engine, Kubernetes dependency resolution — are fundamentally graphs under the hood. If you can't navigate a graph, you can't reason about the systems that run the modern internet.

The challenge with graph interviews isn't memorizing algorithms. BFS and DFS are straightforward once you've seen them twice. The real difficulty is pattern recognition: knowing within 60 seconds whether a problem is a shortest-path problem, a cycle detection problem, a topological sort problem, or a connected-components problem — and then executing flawlessly under pressure without missing the edge cases that separate a 'hire' from a 'no hire'.

By the end of this article you'll have a mental framework for classifying any graph problem on sight, complete runnable solutions for the 10 most common interview problems, and the internal mechanics — why BFS gives shortest paths in unweighted graphs, why DFS finds cycles, why Union-Find beats DFS for dynamic connectivity — explained at the level an interviewer expects when they ask 'can you walk me through your reasoning?'

Breadth-First Search (BFS): The Shortest Path Pattern

Breadth-First Search is the definitive algorithm for finding the shortest path in an unweighted graph. It explores the graph in 'layers'—visiting all neighbors at distance 1, then distance 2, and so on. This guarantees that the first time you reach a target node, you've found the shortest route. In interviews, this pattern is frequently disguised as 'minimum number of moves to solve a puzzle' or 'nearest exit in a maze.'

Here's how the BFS pattern works in practice: you maintain a queue and a visited set. Each layer corresponds to one iteration over the current queue size. For each node popped, you examine its neighbors. If a neighbor hasn't been visited, mark it visited and enqueue it. The distance increments only after processing a full layer. This gives you the correct minimum distance by construction.

A common variant is the 'multi-source BFS' where you start from several nodes simultaneously — used in problems like 'walls and gates' or 'spreading fire.'

io/thecodeforge/graphs/BfsNavigator.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package io.thecodeforge.graphs;

import java.util.*;

public class BfsNavigator {
    /**
     * io.thecodeforge implementation of Shortest Path in an Adjacency List
     * Time Complexity: O(V + E), Space Complexity: O(V)
     */
    public int findShortestPath(int nodes, List<List<Integer>> adj, int start, int target) {
        Queue<Integer> queue = new LinkedList<>();
        boolean[] visited = new boolean[nodes];
        int distance = 0;

        queue.add(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                int current = queue.poll();
                if (current == target) return distance;

                for (int neighbor : adj.get(current)) {
                    if (!visited[neighbor]) {
                        visited[neighbor] = true;
                        queue.add(neighbor);
                    }
                }
            }
            distance++;
        }
        return -1; // No path found
    }
}
Output
// Example: Input 0 -> 2, Distance: 2 moves.
Forge Tip:
Type this code yourself rather than copy-pasting. The muscle memory of writing it will help it stick. Remember: For BFS, always use a Queue. For DFS, use a Stack (or recursion).
Production Insight
BFS is your go-to for shortest paths in unweighted graphs, but it fails when edge weights differ.
In production, systems like network routing or map directions use weighted graphs — BFS would give wrong results.
Rule: BFS = unweighted shortest path; Dijkstra = weighted (non-negative); Bellman-Ford = negative weights.
Key Takeaway
BFS gives shortest paths in unweighted graphs by exploring level by level.
The key invariant: mark visited when enqueued, not when dequeued — preventing duplicate processing.
If you need minimum steps in a grid or moves in a puzzle, BFS is your only first-choice pattern.

Depth-First Search (DFS): Cycle Detection, Connectivity, and Exhaustive Paths

Depth-First Search explores as far as possible along each branch before backtracking. It's the algorithm of choice when you need to explore all possibilities — detecting cycles, finding connected components, or solving problems like 'clone a graph' or 'path exists between two nodes.' DFS can be implemented recursively (simpler) or iteratively with a stack (avoids stack overflow for deep graphs).

Cycle detection using DFS is a classic pattern. For undirected graphs, you need to check if you encounter a neighbor that is visited and is NOT the parent of the current node. For directed graphs, you also need a recursion stack (set) to detect a back edge — a node that is currently in the recursion stack means a cycle.

DFS also forms the backbone of backtracking problems on graphs, like finding all paths from source to target in a DAG. In those cases, you don't mark nodes as visited globally; instead, you mark them on the current path and unmark after recursion.

io/thecodeforge/graphs/DfsCycleDetector.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.thecodeforge.graphs;

import java.util.*;

public class DfsCycleDetector {
    /**
     * Detects cycle in a directed graph using DFS with recursion stack.
     * O(V+E) time, O(V) space.
     */
    public boolean hasCycle(int nodes, List<List<Integer>> adj) {
        int[] state = new int[nodes]; // 0=unvisited, 1=in current stack, 2=processed
        for (int i = 0; i < nodes; i++) {
            if (state[i] == 0) {
                if (dfs(i, adj, state)) return true;
            }
        }
        return false;
    }

    private boolean dfs(int u, List<List<Integer>> adj, int[] state) {
        state[u] = 1;
        for (int v : adj.get(u)) {
            if (state[v] == 1) return true; // back edge => cycle
            if (state[v] == 0 && dfs(v, adj, state)) return true;
        }
        state[u] = 2;
        return false;
    }
}
Output
// Returns true if a cycle exists, false otherwise.
Three-Color DFS Model
  • White (0): node not visited yet.
  • Gray (1): node is currently on the recursion stack (part of current path).
  • Black (2): node and all descendants fully explored.
  • If we encounter a gray node during DFS, a cycle exists.
Production Insight
Recursive DFS can cause StackOverflowError on deep graphs (>10k depth).
In production, always use iterative DFS with an explicit stack for large graphs.
Rule: recursion depth > 1000 => switch to iterative stack.
Key Takeaway
DFS explores all paths — use it for cycle detection and exhaustive search.
Three-color state array avoids infinite loops and detects cycles correctly.
For undirected graphs, pass parent reference to avoid false positive cycles.

Topological Sort: Handling Task Dependencies (Kahn's Algorithm)

Topological sorting is only applicable to Directed Acyclic Graphs (DAGs). It provides a linear ordering of vertices such that for every directed edge $u \to v$, vertex $u$ comes before $v$. This is the foundation of build systems (like Gradle or Maven) and task scheduling. Kahn's Algorithm is the preferred iterative approach using in-degrees.

The algorithm works by maintaining an in-degree array for each node. Initialize a queue with all nodes that have in-degree 0. Then repeatedly pop a node, append it to result, and for each neighbor, decrement its in-degree. If a neighbor's in-degree becomes 0, push it into the queue. If at the end the result size doesn't equal the number of nodes, the graph has a cycle.

The alternative is recursive topological sort using DFS with a list of nodes in post-order — reverse of finishing times gives the topological order.

io/thecodeforge/graphs/KahnAlgorithm.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package io.thecodeforge.graphs;

import java.util.*;

public class DependencyResolver {
    /**
     * Resolves task order using Kahn's Algorithm (Topological Sort)
     */
    public int[] resolveOrder(int numTasks, int[][] dependencies) {
        int[] inDegree = new int[numTasks];
        List<List<Integer>> adj = new ArrayList<>();
        for (int i = 0; i < numTasks; i++) adj.add(new ArrayList<>());

        for (int[] dep : dependencies) {
            adj.get(dep[1]).add(dep[0]);
            inDegree[dep[0]]++;
        }

        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numTasks; i++) {
            if (inDegree[i] == 0) queue.add(i);
        }

        int[] order = new int[numTasks];
        int index = 0;
        while (!queue.isEmpty()) {
            int curr = queue.poll();
            order[index++] = curr;

            for (int neighbor : adj.get(curr)) {
                inDegree[neighbor]--;
                if (inDegree[neighbor] == 0) queue.add(neighbor);
            }
        }

        return index == numTasks ? order : new int[0]; // Empty if cycle detected
    }
}
Output
// Resolves build order: Task 0 -> Task 1 -> Task 2
Edge Case Alert:
Always check for cycles! If the final ordered list doesn't contain all nodes, the graph has a cycle, meaning a circular dependency exists.
Production Insight
Kahn's algorithm with queue is O(V+E) but consumes O(V) memory for in-degree array.
In production dependency resolvers (e.g., Gradle, Maven), cycles cause build failures that waste hours.
Rule: always check result length against node count — a missing node means a cycle.
Key Takeaway
Topological sort = linear ordering respecting edge direction.
Kahn's algorithm uses in-degrees and a queue — simpler than DFS-based topo.
Run cycle detection first if there's any chance of circular dependencies.

Union-Find (Disjoint Set Union): Dynamic Connectivity & Redundant Edges

Union-Find is designed for problems where you need to efficiently merge sets and answer connectivity queries under dynamic edge additions. It excels in scenarios like 'find redundant connection' (detect where an edge connects two already-connected nodes) or 'number of components after adding edges online.'

The core operations are find() — with path compression — and union() — using union by rank or size. Optimized versions run in almost O(1) amortized time (inverse Ackermann function). Union-Find does not handle graph traversal or shortest paths; it's strictly for connectivity.

A classic interview problem: given a list of edges in a graph, find which edge creates a cycle (redundant connection). Using Union-Find, you process edges one by one. For each edge (u, v), if find(u) == find(v), the edge is redundant. Otherwise, union(u, v).

io/thecodeforge/graphs/UnionFind.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package io.thecodeforge.graphs;

public class UnionFind {
    private int[] parent, rank;

    public UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        for (int i = 0; i < n; i++) parent[i] = i;
    }

    public int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]); // path compression
        return parent[x];
    }

    public boolean union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX == rootY) return false; // already connected

        // union by rank
        if (rank[rootX] < rank[rootY]) parent[rootX] = rootY;
        else if (rank[rootX] > rank[rootY]) parent[rootY] = rootX;
        else {
            parent[rootY] = rootX;
            rank[rootX]++;
        }
        return true;
    }

    public boolean isConnected(int x, int y) {
        return find(x) == find(y);
    }
}
Output
// Usage: UnionFind uf = new UnionFind(5); uf.union(0,1); uf.isConnected(0,2); // false
Union-Find as a Forest of Trees
  • find() climbs up the parent pointers to the root, compressing along the way.
  • union() attaches the smaller tree root to the larger tree root (by rank).
  • Time complexity: amortized O(α(n)) where α is the inverse Ackermann function.
  • Useful for: detecting cycles in undirected graphs, number of islands (dynamic version), Kruskals MST algorithm.
Production Insight
Union-Find is memory-efficient (just two arrays) but not suitable for dynamic disconnection.
In production, systems like Kubernetes node connectivity or load balancer health checks use more dynamic methods.
Rule: if you only need connectivity queries after edge additions, Union-Find beats BFS/DFS hands down.
Key Takeaway
Union-Find solves dynamic connectivity problems with near-constant time.
Path compression and union by rank are mandatory for performance — omit them and you're O(n).
Not for paths — only for connectivity checks.

Graph Representation & Pattern Recognition: The First 60 Seconds

The most critical skill in graph interviews is recognising the pattern within the first minute. Here's a quick decision tree:

  1. Need shortest path in unweighted graph? → BFS.
  2. Need all paths or detect cycles? → DFS.
  3. Need to process tasks in order? → topological sort (Kahn/DFS).
  4. Need dynamic connectivity or detect redundant edges? → Union-Find.
  5. Weighted graph, shortest path, no negative edges? → Dijkstra.
  6. Weighted graph with negative edges? → Bellman-Ford.
  7. Need all-pairs shortest paths? → Floyd-Warshall.

Graph representation also matters. Adjacency List (List<List<Integer>>) is the default for sparse graphs (V up to 10^5, E up to 10^5). Adjacency Matrix (int[][]) only if V is small (< 1000) or graph is dense (E ~ V²). Edge List (List<int[]>) is useful when you need to process edges without adjacency queries.

In interviews, you'll rarely need to implement Dijkstra from scratch — but you must be able to reason about its behavior and write BFS/DFS/Kahn without hesitation.

io/thecodeforge/graphs/GraphRepresentation.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.thecodeforge.graphs;

import java.util.*;

public class GraphRepresentation {
    // Adjacency List (preferred)
    public List<List<Integer>> buildAdjList(int n, int[][] edges) {
        List<List<Integer>> adj = new ArrayList<>();
        for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
        for (int[] e : edges) {
            adj.get(e[0]).add(e[1]);
            adj.get(e[1]).add(e[0]); // for undirected
        }
        return adj;
    }

    // Adjacency Matrix (for dense/small)
    public int[][] buildAdjMatrix(int n, int[][] edges) {
        int[][] matrix = new int[n][n];
        for (int[] e : edges) {
            matrix[e[0]][e[1]] = 1;
            matrix[e[1]][e[0]] = 1;
        }
        return matrix;
    }

    // Edge List (for edge processing)
    public int[][] buildEdgeList(int[][] edges) {
        return edges; // already edge list
    }
}
Output
// Choose based on constraints: List for 10^5, matrix for <1000 nodes.
Common Trap:
Don't start coding representation until you've identified the pattern. Many candidates dive into matrix because it looks simpler, but it wastes time and memory for large graphs.
Production Insight
Real-world systems often use adjacency lists with weighted edges in a HashMap per node for sparse graphs.
In production, graph representations must balance memory and query speed — adjacency list wins for most.
Rule: if V > 10^4, use adjacency list. If V < 1000, matrix is fine.
Key Takeaway
Pattern recognition determines success: BFS for shortest, DFS for cycles, Kahn for deps, Union-Find for connectivity.
Adjacency list is your default — it's O(V+E) space and fast traversal.
First 60 seconds: identify pattern and choose the right data structure.
● Production incidentPOST-MORTEMseverity: high

The Build Pipeline That Looped Forever

Symptom
Build jobs for a microservice monorepo never completed. The scheduler kept recompiling the same modules in different orders, producing no output but consuming CPU and memory until the build server OOM-killed the runner.
Assumption
The team assumed the build failure was due to missing artifacts or stale caches. They restarted the pipeline multiple times, losing hours.
Root cause
The build graph contained a cycle — module A depended on B, B depended on C, and C depended on A. The topological sort algorithm (Kahn's) wasn't detecting cycles because the team had disabled cycle detection to 'speed up' the build. The scheduler pushed jobs into an infinite resolution loop.
Fix
Re‑enabled cycle detection in the build scheduler. Added a pre‑check: run a DFS-based cycle detection before invoking Kahn's algorithm. If a cycle exists, fail immediately with the cycle path in the error message.
Key lesson
  • A topological sort without cycle detection is not a topological sort — it's a ticking bomb.
  • Every dependency resolution system must fail fast on cycles. Silent retries are dangerous.
  • Add a dedicated cycle detection pass (DFS + recursion stack) before any DAG processing.
Production debug guideSymptom → Action cheat sheet for common graph failures5 entries
Symptom · 01
Infinite loop during traversal
Fix
Check visited[] initialisation. Ensure every node is marked visited when enqueued (BFS) or when explored (DFS). Inspect recursion depth if using recursion.
Symptom · 02
Wrong shortest path distance returned
Fix
Verify you are using BFS, not DFS. If using Dijkstra, confirm no negative weights exist. Check that distance array is initialised to INF and updated only when a shorter path is found.
Symptom · 03
Cycle detected where there is none
Fix
For directed graphs, use DFS with recursion stack (gray set) — visited alone isn't enough. For undirected graphs, check parent reference to skip the immediate back edge.
Symptom · 04
Topological sort returns empty or partial order
Fix
Check for cycles using Kahn's algorithm: if in-degree queue empties before all nodes processed, a cycle exists. Print the cycle path via DFS.
Symptom · 05
Union-Find operation returns wrong connectivity
Fix
Verify path compression and union by rank are implemented correctly. Test on a small graph with known connectivity. Check that all edges are processed.
★ Graph Interview Debugging Cheat SheetQuick reference for the three graph patterns you'll see most often in interviews.
Need shortest path in unweighted graph
Immediate action
Switch to BFS immediately.
Commands
Queue<Integer> queue = new LinkedList<>();
int[] dist = new int[n]; Arrays.fill(dist, INF); dist[start]=0;
Fix now
Mark visited when enqueuing, not when polling.
Need to detect a cycle in a directed graph+
Immediate action
Use DFS with three colors (white/gray/black).
Commands
int[] state = new int[n]; // 0=unvisited, 1=in-stack, 2=done
if (state[neighbor]==1) cycle detected;
Fix now
Reset state[] before each DFS from different starting nodes.
Need to find connected components dynamically+
Immediate action
Use Union-Find (Disjoint Set Union).
Commands
int[] parent = new int[n]; for (int i=0;i<n;i++) parent[i]=i;
int find(int x) { if (parent[x]!=x) parent[x]=find(parent[x]); return parent[x]; }
Fix now
Always apply path compression in find() or union operations can be O(n).
Graph Algorithm Comparison
AlgorithmCore PatternUse CaseTime ComplexitySpace Complexity
BFSLevel-order traversalShortest path (unweighted), nearest exitO(V+E)O(V)
DFSExhaustive recursion / stackCycle detection, connectivity, all pathsO(V+E)O(V) (recursion stack or explicit stack)
Kahn's (Topo Sort)In-degree processingDependency resolution, schedulingO(V+E)O(V)
Union-FindDisjoint sets with path compressionDynamic connectivity, redundant edgesO(α(V)) amortizedO(V)
DijkstraGreedy / Priority QueueShortest path in weighted (non-negative) graphsO((V+E) log V)O(V)
Bellman-FordEdge relaxation (V-1 iterations)Shortest path with negative weights, detect negative cyclesO(VE)O(V)

Key takeaways

1
You now understand that graph problems are often just 'Matrix or Adjacency List' puzzles in disguise.
2
You've implemented BFS for shortest paths, DFS for cycle detection, Kahn's Algorithm for topological sorting, and Union-Find for dynamic connectivity using io.thecodeforge standards.
3
The secret to graph interviews is choosing the right data structure (Queue vs. Stack vs. Priority Queue) for the traversal pattern.
4
Pattern recognition within 60 seconds separates hires from no-hires
shortest path => BFS, cycles/paths => DFS, dependencies => Topological Sort, connectivity => Union-Find.
5
Practice daily
the forge only works when it's hot 🔥

Common mistakes to avoid

5 patterns
×

Using DFS to find shortest path in unweighted graph

Symptom
DFS finds a path but it's rarely the shortest; in interviews candidates waste time trying to 'optimise' DFS when BFS would solve it in one go.
Fix
Recognise that any problem asking for minimum steps, moves, or distance in an unweighted graph is a BFS problem.
×

Forgetting to mark nodes as visited

Symptom
Infinite loops or stack overflow: the traversal cycles infinitely through the same nodes.
Fix
Mark visited immediately when adding to the queue (BFS) or when entering the node (DFS). Verify visited array is initialised correctly.
×

Not handling disconnected components

Symptom
Traversal only explores one connected component; the answer is incomplete (e.g., counting islands or checking if all nodes are reachable).
Fix
Loop over all nodes; if unvisited, start a new traversal from that node. This ensures you cover every component.
×

Using adjacency matrix for large sparse graphs (V > 10^4)

Symptom
Memory error (Java heap space) or slow performance: matrix requires V^2 memory.
Fix
Stick with adjacency list. Only consider matrix if V < 1000 or graph is extremely dense.
×

Confusing directed vs undirected cycle detection

Symptom
Undirected cycle detection using a visited array alone gives false positives (detects cycle from the edge back to parent).
Fix
For undirected, pass a parent parameter to avoid counting the immediate back edge. For directed, use a recursion stack in addition to visited.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Given a reference of a node in a connected undirected graph, return a de...
Q02SENIOR
You are given a list of intervals where each interval indicates a meetin...
Q03SENIOR
There are n cities and a list of edges representing roads between them. ...
Q01 of 03SENIOR

Given a reference of a node in a connected undirected graph, return a deep copy (clone) of the graph. Explain your approach and write the code.

ANSWER
Use DFS with a HashMap to map original node -> clone node. Start from the given node. If the node is already in the map, return the clone. Otherwise, create a clone, store it, and recursively clone all neighbors. This ensures we don't cycle infinitely. Time O(V+E), space O(V) for the map and recursion stack.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
When should I use BFS over DFS in a graph interview?
02
What is the best way to represent a graph: Adjacency Matrix or Adjacency List?
03
Can Dijkstra's algorithm handle negative weights?
04
What is the difference between topological sort using Kahn's algorithm and using DFS?
05
Is Union-Find suitable for path queries between two nodes?
🔥

That's Coding Patterns. Mark it forged?

4 min read · try the examples if you haven't

Previous
Top 10 DP Interview Problems
5 / 17 · Coding Patterns
Next
Top 10 Linked List Problems