Depth-first order of a graph
A graph has n nodes numbered 0 to n − 1 and a list of undirected
edges, each a pair [u, v]. Traverse it depth-first under these rules, and return the
nodes in the order they are first visited:
- Start at node 0.
- From a node, move to its smallest-numbered unvisited neighbour, and continue from there.
- When a node has no unvisited neighbour, go back to the node you came from and continue from it.
- When the search returns to its start with nothing left, begin again at the smallest-numbered node not yet visited, until every node has been visited.
Examples
| Input | Output | Why |
|---|---|---|
n = 7, edges = [[0,1],[0,2],[1,3],[2,3],[3,4],[5,6]] | [0, 1, 3, 2, 4, 5, 6] | From 3 the search tries 2 before 4. Nodes 5 and 6 are a separate part, reached by restarting. |
n = 3, edges = [] | [0, 1, 2] | No edges: every node is its own start. |
n = 4, edges = [[0,3],[3,2],[2,1]] | [0, 3, 2, 1] | A path walked from one end. |
Constraints
- 1 ≤ n ≤ 200
- 0 ≤ edges.length ≤ n(n − 1)/2
- No self-loops and no repeated edges
Hints
Hint 1
Build an adjacency list first, and sort each node's neighbours so the smallest is tried first.
Hint 2
A recursive function dfs(u) marks u, records it, then calls itself on each unvisited neighbour in order.
Hint 3
Call dfs from every node, in increasing order, that is still unvisited after the previous calls.
Where people lose marks
- Adding each undirected edge in one direction only, so some neighbours are never seen.
- Forgetting the outer loop: on a disconnected graph only the first part is visited.
- Marking a node visited when the search leaves it rather than when it enters, so a cycle visits it twice.
- An iterative version that pushes all neighbours at once visits them in a different order from the recursive one; to match it, push them in reverse or keep a neighbour index per node.
Dry run on 7,[[0,1],[0,2],[1,3],[2,3],[3,4],[5,6]]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 15call stack (bottom → top)00visit order00
Start a new search at 0: mark it visited and push it on the call stack.
-
Step 2 / 15call stack (bottom → top)0011visit order0011
From 0, go to its first unvisited neighbour, 1. Mark it and push it.
-
Step 3 / 15call stack (bottom → top)001132visit order001132
From 1, go to its first unvisited neighbour, 3. Mark it and push it.
-
Step 4 / 15call stack (bottom → top)00113223visit order00113223
From 3, go to its first unvisited neighbour, 2. Mark it and push it.
-
Step 5 / 15call stack (bottom → top)001132visit order00113223
Every neighbour of 2 is visited: 2 is finished and popped, so the search backtracks to 3.
-
Step 6 / 15call stack (bottom → top)00113243visit order0011322344
From 3, go to its first unvisited neighbour, 4. Mark it and push it.
-
Step 7 / 15call stack (bottom → top)001132visit order0011322344
Every neighbour of 4 is visited: 4 is finished and popped, so the search backtracks to 3.
-
Step 8 / 15call stack (bottom → top)0011visit order0011322344
Every neighbour of 3 is visited: 3 is finished and popped, so the search backtracks to 1.
-
Step 9 / 15call stack (bottom → top)00visit order0011322344
Every neighbour of 1 is visited: 1 is finished and popped, so the search backtracks to 0.
-
Step 10 / 15call stack (bottom → top)empty0visit order0011322344
Every neighbour of 0 is visited: 0 is finished and popped.
-
Step 11 / 15call stack (bottom → top)empty0visit order0011322344
The search from the previous start is exhausted but 5 is unvisited: this graph is not connected, so the outer loop starts again at 5.
-
Step 12 / 15call stack (bottom → top)50visit order001132234455
Start a new search at 5: mark it visited and push it on the call stack.
-
Step 13 / 15call stack (bottom → top)5061visit order00113223445566
From 5, go to its first unvisited neighbour, 6. Mark it and push it.
-
Step 14 / 15call stack (bottom → top)50visit order00113223445566
Every neighbour of 6 is visited: 6 is finished and popped, so the search backtracks to 5.
-
Step 15 / 15call stack (bottom → top)empty0visit order00113223445566
Every neighbour of 5 is visited: 5 is finished and popped. Every node is visited. Order: 0, 1, 3, 2, 4, 5, 6.
How to think about it
This is depth-first traversal exactly as the lesson describes it, with the order made unique by trying neighbours smallest first. Two parts:
- The search.
dfs(u)marksuas visited, appends it to the answer, and calls itself on each neighbour that is not yet visited. Marking on entry means a node reached again through a cycle is skipped. - The outer loop. One search covers only the part of the graph connected to its start. Looping over all nodes and searching from each one still unvisited covers every part.
The depth of recursion can reach n. With n ≤ 200 that is safe everywhere; for large graphs, replace the call stack with an explicit stack of (node, next neighbour index) pairs.
Complexity. Time O(n + m log m) · Space O(n + m)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def dfs_order(n, edges):
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u) # undirected: store both directions
for nb in adj:
nb.sort() # smallest neighbour first
seen = [False] * n
order = []
def dfs(u):
seen[u] = True # mark on entry
order.append(u)
for v in adj[u]:
if not seen[v]:
dfs(v)
for s in range(n): # one search per unvisited part
if not seen[s]:
dfs(s)
return order#include <algorithm>
#include <functional>
#include <vector>
std::vector<int> solve(int n, const std::vector<std::vector<int>>& edges) {
std::vector<std::vector<int>> adj(n);
for (const auto& e : edges) {
adj[e[0]].push_back(e[1]);
adj[e[1]].push_back(e[0]); // undirected: store both directions
}
for (auto& nb : adj) std::sort(nb.begin(), nb.end());
std::vector<bool> seen(n, false);
std::vector<int> order;
std::function<void(int)> dfs = [&](int u) {
seen[u] = true; // mark on entry
order.push_back(u);
for (int v : adj[u]) if (!seen[v]) dfs(v);
};
for (int s = 0; s < n; ++s) if (!seen[s]) dfs(s);
return order;
}import java.util.*;
class Solution {
private List<List<Integer>> adj;
private boolean[] seen;
private int[] order;
private int count;
public int[] dfsOrder(int n, int[][] edges) {
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]); // undirected: store both directions
}
for (List<Integer> nb : adj) Collections.sort(nb);
seen = new boolean[n];
order = new int[n];
count = 0;
for (int s = 0; s < n; s++) if (!seen[s]) dfs(s);
return order;
}
private void dfs(int u) {
seen[u] = true; // mark on entry
order[count++] = u;
for (int v : adj.get(u)) if (!seen[v]) dfs(v);
}
}function dfsOrder(n, edges) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
adj[u].push(v);
adj[v].push(u); // undirected: store both directions
}
for (const nb of adj) nb.sort((a, b) => a - b);
const seen = new Array(n).fill(false);
const order = [];
const dfs = u => {
seen[u] = true; // mark on entry
order.push(u);
for (const v of adj[u]) if (!seen[v]) dfs(v);
};
for (let s = 0; s < n; s++) if (!seen[s]) dfs(s);
return order;
}fn solve(n: i64, edges: &[[i64; 2]]) -> Vec<i64> {
let n = n as usize;
let mut adj = vec![Vec::new(); n];
for e in edges {
let (u, v) = (e[0] as usize, e[1] as usize);
adj[u].push(v);
adj[v].push(u); // undirected: store both directions
}
for nb in adj.iter_mut() {
nb.sort();
}
fn dfs(u: usize, adj: &[Vec<usize>], seen: &mut Vec<bool>, order: &mut Vec<i64>) {
seen[u] = true; // mark on entry
order.push(u as i64);
for &v in &adj[u] {
if !seen[v] {
dfs(v, adj, seen, order);
}
}
}
let mut seen = vec![false; n];
let mut order = Vec::with_capacity(n);
for s in 0..n {
if !seen[s] {
dfs(s, &adj, &mut seen, &mut order);
}
}
order
}import Data.List (foldl', sort)
import qualified Data.Map.Strict as M
import qualified Data.Set as S
solve :: Int -> [[Int]] -> [Int]
solve n edges = reverse (snd (foldl' start (S.empty, []) [0 .. n - 1]))
where
adj = M.map sort (M.fromListWith (++) (concat [[(u, [v]), (v, [u])] | [u, v] <- edges]))
-- the state is (visited set, visit order reversed)
start acc@(seen, _) s
| S.member s seen = acc
| otherwise = dfs acc s
dfs (seen, order) u = foldl' visit (S.insert u seen, u : order) (M.findWithDefault [] u adj)
visit acc@(seen, _) v
| S.member v seen = acc
| otherwise = dfs acc v
Variants to try
- Return the nodes in the order the search finishes them (post-order). Reversed, that is a topological order of a DAG.
- Count the separate parts of the graph: the number of times the outer loop starts a search.
- Rewrite the search without recursion so that a path of a million nodes does not overflow the stack.
Next problems
Breadth-first order of a graph
Return the order in which a breadth-first traversal reaches every node: nearest first, ties broken by number, every part of the graph covered.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Easy12 minDoes an undirected graph contain a cycle?
Return true if the graph has a cycle: a search that meets a visited node by any edge but the one it came along has found one.
Open the problem →Write a solution and run it against the real test table.
Keep going
Breadth-first order of a graph
Return the order in which a breadth-first traversal reaches every node: nearest first, ties broken by number, every part of the graph covered.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Easy12 minDoes an undirected graph contain a cycle?
Return true if the graph has a cycle: a search that meets a visited node by any edge but the one it came along has found one.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.