How do you decide between BFS and DFS - and how do you recognise a graph problem that nobody described as a graph?
Choose BFS when the question asks for the fewest steps in an unweighted graph or when you need results in distance order; choose DFS when you are asking a structural question such as connectivity, cycles or topological order, or when the graph is deep and narrow.
What the interviewer is scoring
- Does the candidate tie BFS to shortest path only after establishing that the edges are unweighted
- Whether nodes are marked visited when enqueued rather than when dequeued
- That recursion depth is raised as a real constraint on DFS for large inputs
- Whether the candidate names the states and the transitions explicitly before declaring the problem a graph
- Can the candidate say what changes the moment edge weights stop being equal
Answer
The choice is driven by the question being asked
BFS explores in order of increasing distance from the source, so it answers questions phrased in terms of fewest: fewest moves, minimum number of transformations, shortest chain. That guarantee holds only when every edge costs the same. The instant edges carry different weights, the first time BFS reaches a node is no longer the cheapest time, and you need Dijkstra instead - or, if the only weights are 0 and 1, a double-ended queue where zero-cost edges are pushed to the front and unit-cost edges to the back.
DFS answers structural questions. Is the graph connected, does it contain a cycle, what is a valid ordering of these dependencies, does a path exist at all. It commits to one branch and unwinds, which makes it the natural fit for anything where you need to know what a subtree or a component contains before you can answer, because the post-order return point is where that information becomes available.
Memory is the second axis and it is often the deciding one. BFS holds an entire frontier, which for a branching factor b at depth d is O(b^d) nodes in the worst case; on a wide graph the queue is the thing that runs out. DFS holds only the current path, O(depth), which is why it wins on wide-and-shallow and loses on deep-and-narrow, where the recursion stack overflows somewhere in the low hundreds of thousands of frames on a default JVM thread. If you need DFS on a graph that deep, convert it to an explicit stack rather than raising the thread stack size.
Traversal that gets the details right
int fewestMoves(Map<String, List<String>> graph, String start, String goal) {
Deque<String> queue = new ArrayDeque<>();
Set<String> seen = new HashSet<>();
queue.add(start);
seen.add(start);
int distance = 0;
while (!queue.isEmpty()) {
for (int remaining = queue.size(); remaining > 0; remaining--) { // snapshot the level size
String node = queue.poll();
if (node.equals(goal)) return distance;
for (String next : graph.getOrDefault(node, List.of())) {
if (seen.add(next)) queue.add(next); // add() returns false if already present
}
}
distance++; // one increment per level, not per node
}
return -1;
}
Two details carry the correctness. The level size is captured before the inner loop begins, because the loop is appending to the same queue as it drains it; reading queue.size() inside the condition would run away. And seen.add(next) is doing double duty as test-and-mark in one operation, which is idiomatic and hard to get wrong by accident.
DFS is shorter but needs the same discipline about where the mark goes:
void dfs(int node, List<List<Integer>> adj, boolean[] visited) {
visited[node] = true; // mark on entry, before recursing
for (int next : adj.get(node)) {
if (!visited[next]) dfs(next, adj, visited);
}
}
Marking visited at the wrong moment
The single most common defect in a written BFS is marking a node visited when it is dequeued rather than when it is enqueued. The traversal still terminates and still returns the right distance, so it passes the small test case and looks fine. What it does is enqueue the same node once per incoming edge, so the queue holds up to O(E) entries instead of O(V), and on a dense graph the memory and the constant factor both blow up. The check-on-dequeue variant then needs a second visited test at the top of the loop to avoid re-expanding, and candidates who add that test have usually reinvented a broken version of the correct thing.
The mirror-image error in DFS is marking after the recursive call rather than before, which on any graph containing a cycle recurses forever. Both errors come from the same place: treating the visited set as a record of what has been processed rather than as a record of what has been committed to.
Naming the nodes out loud
Interview graph problems are rarely handed over as an adjacency list. They arrive as a grid of cells, a set of word transformations, a lock with four dials, a list of build dependencies, a currency exchange table. The recognition test is mechanical: can you finish the sentence "a node is ___, and there is an edge from A to B when ___". If you can, everything you know about traversal applies, and you never have to build the adjacency structure at all - you generate neighbours on demand, which is what makes a state space of millions of lock combinations tractable.
Two vocabulary cues are worth listening for. "Minimum number of steps" over uniform moves means BFS almost every time. "Order these such that each comes after its prerequisites" means a topological sort, which means a directed graph and a cycle check, because the ordering only exists if the graph is acyclic. A candidate who states the nodes and edges before writing any code has done the part that transfers; the traversal itself is the part you can look up.
Likely follow-ups
- The edges now have weights of 0 or 1. What is the cheapest change to your BFS?
- How would you detect a cycle in a directed graph, and why is the undirected version different?
- Your DFS overflows the stack on a graph with a million nodes. What do you do?
- Give me a problem you have seen that was a graph problem in disguise, and name its nodes and edges.
Related questions
- You are given a set of tasks and their prerequisites. Produce a valid execution order, or report that none exists.mediumAlso on graphs6 min
- How would you design a thread-safe component, and why is adding synchronized to every method not a design?hardSame kind of round: concept7 min
- How do you tell whether a value escapes to the heap, and how would you find the allocations that are costing you?hardSame kind of round: concept6 min
- How do goroutines leak, and how would you find a leak in a running service?mediumSame kind of round: concept5 min
- Your function returns nil on the success path, the caller checks err != nil, and the check fires anyway. What happened?hardSame kind of round: concept4 min
- Design rejected the native control, so you are building a custom one. How do you make it accessible?hardSame kind of round: concept5 min
- A client hangs up halfway through a request. How do you structure a Go HTTP service so it stops doing the work?mediumSame kind of round: concept7 min
- You run ten coroutines concurrently and one of them raises. What happens to the other nine?hardSame kind of round: concept5 min