The two fundamental graph and tree traversal strategies — go deep or go wide.
Depth-First Search (DFS) explores as far as possible along each branch before backtracking. It uses a stack (or recursion, which is an implicit stack). Breadth-First Search (BFS) explores all neighbors at the current depth before moving to the next level. It uses a queue.
Same tree, two completely different traversal orders:
The numbers on each node show the visit order. Notice how DFS dives deep immediately while BFS sweeps level by level.
DFS is like exploring a maze by always taking the leftmost turn until you hit a dead end, then backtracking. BFS is like dropping a stone in water — the wavefront expands outward uniformly. Both visit every reachable node exactly once.
DFS is your go-to when the problem involves exploring all possibilities, working with tree structures, or when path information matters more than distance.
Path finding in trees
Cycle detection
Topological sort
Exhaustive search / backtracking
Tree problems (most)
Connected components
Rule of thumb: If you need to explore an entire subtree or all paths from a node, DFS is usually simpler and more memory-efficient (stack depth = tree height, not tree width).
BFS shines when you need the shortest path or minimum number of steps in unweighted graphs and grids.
Shortest path (unweighted)
Level-order traversal
Minimum steps / moves
Nearest neighbor
The moment you see "shortest", "minimum steps", or "nearest" in an unweighted graph problem, reach for BFS. DFS might find a path, but it will not guarantee it is the shortest one.
DFS on a binary tree comes in three flavors depending on when you process the current node relative to its children: preorder (before), inorder (between), and postorder (after).
Same tree, same DFS, but the processing happens at a different point in the recursion.
// Recursive DFS — all three orders in one structure void dfs(TreeNode node) { if (node == null) return; // preorder: process node HERE (before children) dfs(node.left); // inorder: process node HERE (between children) dfs(node.right); // postorder: process node HERE (after children) }
void dfsIterative(TreeNode root) { if (root == null) return; Deque<TreeNode> stack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); // process node if (node.right != null) stack.push(node.right); // right first! if (node.left != null) stack.push(node.left); // so left is popped first } }
When to use which order? Preorder for serialization and copying trees. Inorder for BST problems (gives sorted order). Postorder for computing sizes, heights, or any bottom-up aggregation.
BFS on a tree processes nodes level by level. The key technique is the "queue + size" pattern: at each level, record the current queue size, then process exactly that many nodes. This cleanly separates levels.
List<List<Integer>> levelOrder(TreeNode root) { List<List<Integer>> result = new ArrayList<>(); if (root == null) return result; Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); // nodes at this level List<Integer> level = new ArrayList<>(); for (int i = 0; i < size; i++) { TreeNode node = queue.poll(); level.add(node.val); // process node if (node.left != null) queue.offer(node.left); if (node.right != null) queue.offer(node.right); } result.add(level); } return result; }
The int size = queue.size() line is critical. Without it, you cannot tell where one level ends and the next begins. This one-line trick enables level-aware BFS and is used in nearly every BFS tree problem.
Unlike trees, graphs can have cycles. The critical difference: you must maintain a visited set to avoid infinite loops. The structure is otherwise identical to tree DFS.
Starting from node 0, DFS explores deep before backtracking. Gray = currently in stack. Black = fully processed.
// DFS on a graph with adjacency list void dfs(int node, List<List<Integer>> graph, boolean[] visited) { visited[node] = true; // process node for (int neighbor : graph.get(node)) { if (!visited[neighbor]) { dfs(neighbor, graph, visited); } } }
// DFS on a grid — e.g., Number of Islands int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; void dfs(char[][] grid, int r, int c) { if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length || grid[r][c] != '1') return; grid[r][c] = '0'; // mark visited for (int[] d : dirs) { dfs(grid, r + d[0], c + d[1]); } }
Visited tracking options: (1) A separate boolean[] or Set. (2) Modify the input in-place (like marking '1' as '0' in grids). (3) Three-state coloring (white/gray/black) for cycle detection in directed graphs.
BFS on grids is one of the most common interview patterns. The wavefront expands outward from the source, guaranteeing the shortest path in unweighted grids.
Starting from S, BFS explores all cells at distance 1, then distance 2, etc. Each ring of numbers represents one BFS layer.
// BFS shortest path on a grid int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}}; int shortestPath(int[][] grid, int[] start, int[] end) { int rows = grid.length, cols = grid[0].length; boolean[][] visited = new boolean[rows][cols]; Queue<int[]> queue = new LinkedList<>(); queue.offer(start); visited[start[0]][start[1]] = true; int steps = 0; while (!queue.isEmpty()) { int size = queue.size(); // all nodes at this distance for (int i = 0; i < size; i++) { int[] pos = queue.poll(); if (pos[0] == end[0] && pos[1] == end[1]) return steps; // found shortest path! for (int[] d : dirs) { int nr = pos[0] + d[0], nc = pos[1] + d[1]; if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && !visited[nr][nc] && grid[nr][nc] != 1) { // not a wall visited[nr][nc] = true; queue.offer(new int[]{nr, nc}); } } } steps++; } return -1; // unreachable }
Mark visited when enqueuing, not when dequeuing. This is a common mistake. If you wait to mark visited until you poll, you may add the same cell to the queue multiple times from different neighbors, wasting time and space.
| Aspect | DFS | BFS |
|---|---|---|
| Data Structure | Stack (or recursion) | Queue |
| Traversal Order | Deepest nodes first | Nearest nodes first |
| Shortest Path? | No guarantee | Yes (unweighted) |
| Space (Tree) | O(h) — height | O(w) — max width |
| Space (Graph) | O(V) | O(V) |
| Best For | Paths, cycles, backtracking, exhaustive search | Shortest path, level order, minimum steps |
| Implementation | Simpler (recursion) | Always iterative |
| Risk | Stack overflow on deep graphs | High memory on wide graphs |
For balanced binary trees, DFS uses O(log n) space while BFS uses O(n) space (the last level has n/2 nodes). For extremely deep, narrow graphs (like a linked list), BFS uses O(1) space while DFS uses O(n). Choose based on the shape of your data.
Classic LeetCode problems organized by which traversal strategy to use. Many problems can be solved with either DFS or BFS, but one is usually cleaner.
Practice tip: Start with #104 (pure recursive DFS) and #102 (pure BFS template). Then tackle #200 both ways — DFS with flood fill and BFS with a queue. Once comfortable, move to #207 for topological sort and #994 for multi-source BFS.
Step through both traversal strategies to see exactly how each one explores nodes differently.
Click "Step" to watch DFS (preorder) explore a binary tree. Nodes are colored as they are visited.
Watch BFS find the shortest path from S to E. The wavefront expands outward layer by layer.
Both DFS and BFS visit every vertex exactly once — that is O(V). For each vertex, they examine all its edges — summed across all vertices, that is O(E). Total: O(V + E).
On a grid (R rows, C cols): V = R * C and each cell has at most 4 edges, so E = O(V). Time becomes O(R * C).
Space: The visited set takes O(V). DFS additionally uses O(V) stack space in the worst case (linear graph). BFS uses O(V) queue space in the worst case (all nodes at same level). Either way: O(V).
DFS = stack, BFS = queue. This is the single most important distinction. DFS goes deep using LIFO, BFS goes wide using FIFO. Everything else follows from this.
BFS guarantees shortest path in unweighted graphs. The first time BFS reaches a node, it has found the minimum number of edges. DFS cannot make this guarantee.
Always track visited nodes in graphs. Trees are acyclic by definition, so you do not need a visited set. Graphs can have cycles, so forgetting the visited set means infinite loops.
DFS maps naturally to recursion. Most tree problems are cleanest with recursive DFS. But watch the stack depth — for very deep graphs (over 10,000 levels), convert to iterative DFS to avoid stack overflow.
The "queue + size" pattern is BFS's most important technique. Capturing queue.size() at the start of each level lets you process nodes level by level. This powers level-order traversal, shortest path counting, and multi-source BFS.
Many problems can use either — pick the simpler one. Number of Islands works with both DFS and BFS. Clone Graph works with both. Default to DFS for tree problems and BFS for shortest-path problems, then choose the cleaner option for everything else.