Graphs: BFS & DFS
Visit everything reachable from a start node, ring by ring with a queue (BFS) or path by path with a stack (DFS), in O(V + E) time.
How It Works
A graph is a set of nodes joined by edges: cities and roads, users and follows, cells in a grid and the cells beside them. Most graph problems start the same way: visit everything you can reach from a starting node, once each. Breadth-first search (BFS) and depth-first search (DFS) are the two standard orders for doing that. Both run in O(V + E) for V nodes and E edges, because each node is visited once and each node's neighbor list is scanned once.
First you need the graph in a shape you can walk. Problems usually hand you an edge list like [[0, 1], [0, 3], ...], which can't tell you a node's neighbors without scanning every edge. Convert it to an adjacency list: for each node, the list of nodes it connects to. That takes O(V + E) space and lists a node's neighbors in time proportional to how many it has. The alternative is an adjacency matrix, a V × V table where matrix[u][v] says whether the edge exists. It answers "is there an edge from u to v?" in O(1), but it costs O(V²) space even when there are few edges, and listing a node's neighbors means scanning a whole row, so a traversal becomes O(V²). Use the list unless the graph is small and dense or you need constant-time edge checks.
graph adjacency list
1 --- 2 0: [1, 3]
/ \ 1: [0, 2]
0 5 6 2: [1, 5]
\ / 3: [0, 4]
3 --- 4 4: [3, 5]
5: [2, 4]
6: []BFS keeps a first-in, first-out queue. It visits the start, then every node one edge away, then every node two edges away, and so on outward. Because it reaches nodes in order of distance, the first time it sees a node is along a path with the fewest possible edges. That makes BFS the tool for shortest paths when every edge costs the same. DFS follows one path as far as it can, then backs up to the most recent node that still has an unexplored neighbor. It uses a stack, usually the call stack through recursion, and fits problems about exploring whole regions, finding any path, detecting cycles and ordering dependencies. Both mark each node as seen, because without that a single cycle would send the search around forever.
Breadth-First Search, Step by Step
Step 1 of 12
graph
queue
- Being expanded, or an edge that finds a new node
- Waiting in the queue
- Expanded, or the edge that found a node
- Already seen, or never reached
Start at 0 with dist[0] = 0. Every other distance is -1, meaning not seen yet, and 0 is the only node in the queue.
All 12 steps as text
- Start at
0withdist[0] = 0. Every other distance is-1, meaning not seen yet, and0is the only node in the queue. - Dequeue
0from the front. The template dequeues by movingheadforward, so nothing in the array has to shift. 1and3haven't been seen, so each getsdist = 0 + 1 = 1and joins the back of the queue. They're marked now, so no other node can queue them again.- Dequeue
1, the first node at distance1. Every node at distance0is already expanded, because BFS finishes one ring before it starts the next. 0was seen already, so skip it.2is new:dist[2] = 1 + 1 = 2, and it joins the back of the queue behind3.- Dequeue
3, also at distance1. The queue only ever holds two neighboring rings at once, with the nearer one in front. 0was seen already, so skip it.4is new:dist[4] = 1 + 1 = 2, and it joins the back of the queue behind2.- Dequeue
2, the first node at distance2. Every node at distance1is already expanded, because BFS finishes one ring before it starts the next. 1was seen already, so skip it.5is new:dist[5] = 2 + 1 = 3, and it joins the back of the queue behind4.- Dequeue
4.3and5were both seen, and5is still waiting in the queue. Marking a node as it joins the queue is what stops it joining twice. - Dequeue
5.2and4were both seen already, so nothing new joins the queue. - The queue is empty, so the search is done:
dist = [0, 1, 2, 1, 2, 3, -1].6has no edges and stays at-1. Read front to back, the queue is the BFS order[0, 1, 3, 2, 4, 5].
DFS with your own stack runs the same loop with a stack where BFS has its queue. Two changes make it visit nodes in exactly the order recursion would: it marks a node when it's popped, not when it's pushed, and it pushes neighbors in reverse so the first one comes off first.
Depth-First Search With a Stack
Step 1 of 9
graph
stack
order
- Just popped, or an edge that pushes a node
- Waiting on the stack
- Visited, or the edge it was reached by
- Already visited, or never reached
Push the start, 0. Unlike BFS, nothing is marked yet: DFS marks a node when it's popped, not when it's pushed, so a deeper path can still reach it first.
All 9 steps as text
- Push the start,
0. Unlike BFS, nothing is marked yet: DFS marks a node when it's popped, not when it's pushed, so a deeper path can still reach it first. - Pop
0and visit it. Push its unseen neighbors in reverse,3then1, so1, the first in its list, ends up on top and is explored first. - Pop
1and visit it.0is visited already, so push only2. It lands on top, so DFS keeps going deeper while3waits underneath. - Pop
2and visit it.1is visited already, so push only5. It lands on top, so DFS keeps going deeper while3waits underneath. - Pop
5and visit it.2is visited already, so push only4. It lands on top, so DFS keeps going deeper while3waits underneath. - Pop
4and visit it.3is already on the stack from0but isn't visited yet, so push it again. The new copy is on top, so3is reached from4, as recursion would reach it. - Pop
3and visit it.0and4are both visited already, so nothing is pushed, and DFS backs up to whatever is underneath. - Pop
3again. This older copy was pushed by0, but3was already visited from4, so skip it. Copies like this are why the stack can grow to O(E) entries, not just O(V). - The stack is empty:
[0, 1, 2, 5, 4, 3], the same order recursivedfsOrdergives.6has no edges, so nothing ever pushed it.
A grid is a graph you never have to build. Each cell is a node, and its neighbors are the cells directly up, down, left and right (diagonals too, if the problem says so). Instead of an adjacency list, loop over a small array of offsets such as [[1, 0], [-1, 0], [0, 1], [0, -1]] and skip any neighbor that falls outside the grid. An R × C grid has R × C nodes and at most four edges per node, so a full traversal costs O(R × C).
When to Reach for It
- The input is an edge list, an adjacency list, or a grid whose cells connect to their neighbors.
- You need to know what's reachable from somewhere, or to count connected groups such as islands, provinces or friend circles.
- The question asks for the fewest moves, steps or hops, and every move costs the same. That's BFS.
- Something spreads outward one step at a time from one or more starting points: fire, rot, infection, a flood.
- You need to explore every path, detect a cycle or order things by dependency. That's DFS.
Complexity
| Operation | Time | Space |
|---|---|---|
| Build an adjacency list from E edges | O(V + E) | O(V + E) |
| BFS or DFS over an adjacency list | O(V + E) | O(V) for seen and the queue or stack, O(V + E) if DFS marks on pop |
| BFS or DFS over an adjacency matrix | O(V²) | O(V²) for the matrix |
| BFS or DFS over an R × C grid | O(R × C) | O(R × C) |
| Check whether edge (u, v) exists | O(deg(u)) with a list, O(1) with a matrix | O(1) |
The Template
Breadth-First and Depth-First Search
buildGraph turns an edge list into an adjacency list, storing each undirected edge in both directions. Each node's neighbors stay in the order the edges were given, so every search visits them in the same order and prints the same result in every language. The four searches share one skeleton: a seen marker per node and a container of nodes still to explore.
bfsOrderuses a queue. A node is marked as seen when it's enqueued, not when it's dequeued, so a second neighbor can't add it again while it waits in line.dfsOrderis recursive.dfsVisitrecords a node, then fully explores its first unseen neighbor before it looks at the second. The call stack plays the role of the DFS stack.dfsOrderIterativegets the same order from a stack of its own. It pushes a node's neighbors in reverse, so the first one is on top. It marks nodes as seen when they're popped, because a node pushed early can be reached again from deeper down, and recursion would visit it from there. So the newer copy wins, and the older one is skipped when it surfaces. Marking on push still visits every node once, which is all Number of Islands needs, but the order is no longer depth-first.bfsDistancesis the same BFS, but it stores a distance instead of a flag, with-1meaning not reached.dist[neighbor] = dist[node] + 1is final the moment it's written, because BFS discovers nodes in order of distance.
BFS from 0, one ring at a time: distance 0 0 distance 1 1, 3 distance 2 2, 4 distance 3 5 DFS from 0, as deep as it can go: 0 -> 1 -> 2 -> 5 -> 4 -> 3
Time O(V + E)Space O(V + E)
from collections import deque
def build_graph(n: int, edges: list[list[int]]) -> list[list[int]]:
"""Adjacency list for an undirected graph with nodes 0..n-1. Each node's
neighbors keep the order the edges were given, which fixes the visit order."""
graph: list[list[int]] = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u) # Undirected: store the edge in both directions.
return graph
def bfs_order(graph: list[list[int]], start: int) -> list[int]:
"""Nodes in the order breadth-first search reaches them from `start`."""
seen = [False] * len(graph)
order = []
queue = deque([start])
seen[start] = True
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
if not seen[neighbor]:
seen[neighbor] = True # Mark on enqueue so no node is queued twice.
queue.append(neighbor)
return order
def dfs_order(graph: list[list[int]], start: int) -> list[int]:
"""Nodes in the order a recursive depth-first search reaches them from `start`."""
seen = [False] * len(graph)
order: list[int] = []
dfs_visit(graph, start, seen, order)
return order
def dfs_visit(graph: list[list[int]], node: int, seen: list[bool], order: list[int]) -> None:
seen[node] = True
order.append(node)
for neighbor in graph[node]:
# Go all the way down this neighbor before trying the next one.
if not seen[neighbor]:
dfs_visit(graph, neighbor, seen, order)
def dfs_order_iterative(graph: list[list[int]], start: int) -> list[int]:
"""The same order as dfs_order, with your own stack in place of the call stack."""
seen = [False] * len(graph)
order: list[int] = []
stack = [start]
while stack:
node = stack.pop()
# A node can wait on the stack more than once. Visit only the first copy popped.
if seen[node]:
continue
# Mark on pop, not on push. A node pushed early may be reached again from
# deeper down, and recursion would visit it from there.
seen[node] = True
order.append(node)
# Push neighbors in reverse, so the first one comes off the stack first.
for neighbor in reversed(graph[node]):
if not seen[neighbor]:
stack.append(neighbor)
return order
def bfs_distances(graph: list[list[int]], start: int) -> list[int]:
"""Fewest edges from `start` to every node, or -1 where no path exists."""
dist = [-1] * len(graph) # -1 also means "not seen yet".
dist[start] = 0
queue = deque([start])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if dist[neighbor] == -1:
# BFS reaches nodes in order of distance, so the first visit is the shortest.
dist[neighbor] = dist[node] + 1
queue.append(neighbor)
return dist
if __name__ == "__main__":
graph = build_graph(7, [[0, 1], [0, 3], [1, 2], [3, 4], [2, 5], [4, 5]])
print(bfs_order(graph, 0)) # [0, 1, 3, 2, 4, 5]
print(dfs_order(graph, 0)) # [0, 1, 2, 5, 4, 3]
print(dfs_order_iterative(graph, 0)) # [0, 1, 2, 5, 4, 3]
print(bfs_distances(graph, 0)) # [0, 1, 2, 1, 2, 3, -1]// Adjacency list for an undirected graph with nodes 0..n-1. Each node's
// neighbors keep the order the edges were given, which fixes the visit order.
export function buildGraph(n: number, edges: number[][]): number[][] {
const graph: number[][] = Array.from({ length: n }, () => []);
for (const [u, v] of edges) {
graph[u].push(v);
graph[v].push(u); // Undirected: store the edge in both directions.
}
return graph;
}
// Nodes in the order breadth-first search reaches them from start.
export function bfsOrder(graph: number[][], start: number): number[] {
const seen = new Array<boolean>(graph.length).fill(false);
const order: number[] = [];
const queue = [start];
seen[start] = true;
let head = 0; // Dequeue by moving a head index, because shift() is O(n).
while (head < queue.length) {
const node = queue[head++];
order.push(node);
for (const neighbor of graph[node]) {
if (!seen[neighbor]) {
seen[neighbor] = true; // Mark on enqueue so no node is queued twice.
queue.push(neighbor);
}
}
}
return order;
}
// Nodes in the order a recursive depth-first search reaches them from start.
export function dfsOrder(graph: number[][], start: number): number[] {
const seen = new Array<boolean>(graph.length).fill(false);
const order: number[] = [];
dfsVisit(graph, start, seen, order);
return order;
}
function dfsVisit(graph: number[][], node: number, seen: boolean[], order: number[]): void {
seen[node] = true;
order.push(node);
for (const neighbor of graph[node]) {
// Go all the way down this neighbor before trying the next one.
if (!seen[neighbor]) dfsVisit(graph, neighbor, seen, order);
}
}
// The same order as dfsOrder, with your own stack in place of the call stack.
export function dfsOrderIterative(graph: number[][], start: number): number[] {
const seen = new Array<boolean>(graph.length).fill(false);
const order: number[] = [];
const stack = [start];
while (stack.length > 0) {
const node = stack.pop()!;
// A node can wait on the stack more than once. Visit only the first copy popped.
if (seen[node]) continue;
// Mark on pop, not on push. A node pushed early may be reached again from
// deeper down, and recursion would visit it from there.
seen[node] = true;
order.push(node);
// Push neighbors in reverse, so the first one comes off the stack first.
for (let i = graph[node].length - 1; i >= 0; i--) {
const neighbor = graph[node][i];
if (!seen[neighbor]) stack.push(neighbor);
}
}
return order;
}
// Fewest edges from start to every node, or -1 where no path exists.
export function bfsDistances(graph: number[][], start: number): number[] {
const dist = new Array<number>(graph.length).fill(-1); // -1 also means "not seen yet".
dist[start] = 0;
const queue = [start];
let head = 0;
while (head < queue.length) {
const node = queue[head++];
for (const neighbor of graph[node]) {
if (dist[neighbor] === -1) {
// BFS reaches nodes in order of distance, so the first visit is the shortest.
dist[neighbor] = dist[node] + 1;
queue.push(neighbor);
}
}
}
return dist;
}
const graph = buildGraph(7, [[0, 1], [0, 3], [1, 2], [3, 4], [2, 5], [4, 5]]);
console.log(JSON.stringify(bfsOrder(graph, 0))); // [0,1,3,2,4,5]
console.log(JSON.stringify(dfsOrder(graph, 0))); // [0,1,2,5,4,3]
console.log(JSON.stringify(dfsOrderIterative(graph, 0))); // [0,1,2,5,4,3]
console.log(JSON.stringify(bfsDistances(graph, 0))); // [0,1,2,1,2,3,-1]package main
import "fmt"
// buildGraph returns the adjacency list for an undirected graph with nodes
// 0..n-1. Each node's neighbors keep the order the edges were given, which
// fixes the visit order.
func buildGraph(n int, edges [][]int) [][]int {
graph := make([][]int, n)
for _, e := range edges {
u, v := e[0], e[1]
graph[u] = append(graph[u], v)
graph[v] = append(graph[v], u) // Undirected: store the edge in both directions.
}
return graph
}
// bfsOrder returns nodes in the order breadth-first search reaches them from start.
func bfsOrder(graph [][]int, start int) []int {
seen := make([]bool, len(graph))
order := []int{}
queue := []int{start}
seen[start] = true
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
order = append(order, node)
for _, neighbor := range graph[node] {
if !seen[neighbor] {
seen[neighbor] = true // Mark on enqueue so no node is queued twice.
queue = append(queue, neighbor)
}
}
}
return order
}
// dfsOrder returns nodes in the order a recursive depth-first search reaches
// them from start.
func dfsOrder(graph [][]int, start int) []int {
seen := make([]bool, len(graph))
return dfsVisit(graph, start, seen, []int{})
}
func dfsVisit(graph [][]int, node int, seen []bool, order []int) []int {
seen[node] = true
order = append(order, node)
for _, neighbor := range graph[node] {
// Go all the way down this neighbor before trying the next one.
if !seen[neighbor] {
order = dfsVisit(graph, neighbor, seen, order)
}
}
return order
}
// dfsOrderIterative returns the same order as dfsOrder, using your own stack
// in place of the call stack.
func dfsOrderIterative(graph [][]int, start int) []int {
seen := make([]bool, len(graph))
order := []int{}
stack := []int{start}
for len(stack) > 0 {
node := stack[len(stack)-1]
stack = stack[:len(stack)-1]
// A node can wait on the stack more than once. Visit only the first copy popped.
if seen[node] {
continue
}
// Mark on pop, not on push. A node pushed early may be reached again from
// deeper down, and recursion would visit it from there.
seen[node] = true
order = append(order, node)
// Push neighbors in reverse, so the first one comes off the stack first.
for i := len(graph[node]) - 1; i >= 0; i-- {
neighbor := graph[node][i]
if !seen[neighbor] {
stack = append(stack, neighbor)
}
}
}
return order
}
// bfsDistances returns the fewest edges from start to every node, or -1 where
// no path exists.
func bfsDistances(graph [][]int, start int) []int {
dist := make([]int, len(graph))
for i := range dist {
dist[i] = -1 // -1 also means "not seen yet".
}
dist[start] = 0
queue := []int{start}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
for _, neighbor := range graph[node] {
if dist[neighbor] == -1 {
// BFS reaches nodes in order of distance, so the first visit is the shortest.
dist[neighbor] = dist[node] + 1
queue = append(queue, neighbor)
}
}
}
return dist
}
func main() {
graph := buildGraph(7, [][]int{{0, 1}, {0, 3}, {1, 2}, {3, 4}, {2, 5}, {4, 5}})
fmt.Println(bfsOrder(graph, 0)) // [0 1 3 2 4 5]
fmt.Println(dfsOrder(graph, 0)) // [0 1 2 5 4 3]
fmt.Println(dfsOrderIterative(graph, 0)) // [0 1 2 5 4 3]
fmt.Println(bfsDistances(graph, 0)) // [0 1 2 1 2 3 -1]
}using System;
using System.Collections.Generic;
public class Solution
{
// Adjacency list for an undirected graph with nodes 0..n-1. Each node's
// neighbors keep the order the edges were given, which fixes the visit order.
public static List<List<int>> BuildGraph(int n, int[][] edges)
{
var graph = new List<List<int>>();
for (int i = 0; i < n; i++) graph.Add(new List<int>());
foreach (var edge in edges)
{
int u = edge[0], v = edge[1];
graph[u].Add(v);
graph[v].Add(u); // Undirected: store the edge in both directions.
}
return graph;
}
// Nodes in the order breadth-first search reaches them from start.
public static List<int> BfsOrder(List<List<int>> graph, int start)
{
var seen = new bool[graph.Count];
var order = new List<int>();
var queue = new Queue<int>();
queue.Enqueue(start);
seen[start] = true;
while (queue.Count > 0)
{
int node = queue.Dequeue();
order.Add(node);
foreach (int neighbor in graph[node])
{
if (!seen[neighbor])
{
seen[neighbor] = true; // Mark on enqueue so no node is queued twice.
queue.Enqueue(neighbor);
}
}
}
return order;
}
// Nodes in the order a recursive depth-first search reaches them from start.
public static List<int> DfsOrder(List<List<int>> graph, int start)
{
var seen = new bool[graph.Count];
var order = new List<int>();
DfsVisit(graph, start, seen, order);
return order;
}
static void DfsVisit(List<List<int>> graph, int node, bool[] seen, List<int> order)
{
seen[node] = true;
order.Add(node);
foreach (int neighbor in graph[node])
{
// Go all the way down this neighbor before trying the next one.
if (!seen[neighbor]) DfsVisit(graph, neighbor, seen, order);
}
}
// The same order as DfsOrder, with your own stack in place of the call stack.
public static List<int> DfsOrderIterative(List<List<int>> graph, int start)
{
var seen = new bool[graph.Count];
var order = new List<int>();
var stack = new Stack<int>();
stack.Push(start);
while (stack.Count > 0)
{
int node = stack.Pop();
// A node can wait on the stack more than once. Visit only the first copy popped.
if (seen[node]) continue;
// Mark on pop, not on push. A node pushed early may be reached again from
// deeper down, and recursion would visit it from there.
seen[node] = true;
order.Add(node);
// Push neighbors in reverse, so the first one comes off the stack first.
for (int i = graph[node].Count - 1; i >= 0; i--)
{
int neighbor = graph[node][i];
if (!seen[neighbor]) stack.Push(neighbor);
}
}
return order;
}
// Fewest edges from start to every node, or -1 where no path exists.
public static int[] BfsDistances(List<List<int>> graph, int start)
{
var dist = new int[graph.Count];
Array.Fill(dist, -1); // -1 also means "not seen yet".
dist[start] = 0;
var queue = new Queue<int>();
queue.Enqueue(start);
while (queue.Count > 0)
{
int node = queue.Dequeue();
foreach (int neighbor in graph[node])
{
if (dist[neighbor] == -1)
{
// BFS reaches nodes in order of distance, so the first visit is the shortest.
dist[neighbor] = dist[node] + 1;
queue.Enqueue(neighbor);
}
}
}
return dist;
}
public static void Main()
{
var graph = BuildGraph(7, [[0, 1], [0, 3], [1, 2], [3, 4], [2, 5], [4, 5]]);
Console.WriteLine(Format(BfsOrder(graph, 0))); // [0, 1, 3, 2, 4, 5]
Console.WriteLine(Format(DfsOrder(graph, 0))); // [0, 1, 2, 5, 4, 3]
Console.WriteLine(Format(DfsOrderIterative(graph, 0))); // [0, 1, 2, 5, 4, 3]
Console.WriteLine(Format(BfsDistances(graph, 0))); // [0, 1, 2, 1, 2, 3, -1]
}
static string Format(IEnumerable<int> values) => "[" + string.Join(", ", values) + "]";
}import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Solution {
// Adjacency list for an undirected graph with nodes 0..n-1. Each node's
// neighbors keep the order the edges were given, which fixes the visit order.
static List<List<Integer>> buildGraph(int n, int[][] edges) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
for (int[] edge : edges) {
int u = edge[0], v = edge[1];
graph.get(u).add(v);
graph.get(v).add(u); // Undirected: store the edge in both directions.
}
return graph;
}
// Nodes in the order breadth-first search reaches them from start.
static List<Integer> bfsOrder(List<List<Integer>> graph, int start) {
boolean[] seen = new boolean[graph.size()];
List<Integer> order = new ArrayList<>();
ArrayDeque<Integer> queue = new ArrayDeque<>();
queue.add(start);
seen[start] = true;
while (!queue.isEmpty()) {
int node = queue.poll();
order.add(node);
for (int neighbor : graph.get(node)) {
if (!seen[neighbor]) {
seen[neighbor] = true; // Mark on enqueue so no node is queued twice.
queue.add(neighbor);
}
}
}
return order;
}
// Nodes in the order a recursive depth-first search reaches them from start.
static List<Integer> dfsOrder(List<List<Integer>> graph, int start) {
boolean[] seen = new boolean[graph.size()];
List<Integer> order = new ArrayList<>();
dfsVisit(graph, start, seen, order);
return order;
}
static void dfsVisit(List<List<Integer>> graph, int node, boolean[] seen, List<Integer> order) {
seen[node] = true;
order.add(node);
for (int neighbor : graph.get(node)) {
// Go all the way down this neighbor before trying the next one.
if (!seen[neighbor]) dfsVisit(graph, neighbor, seen, order);
}
}
// The same order as dfsOrder, with your own stack in place of the call stack.
static List<Integer> dfsOrderIterative(List<List<Integer>> graph, int start) {
boolean[] seen = new boolean[graph.size()];
List<Integer> order = new ArrayList<>();
ArrayDeque<Integer> stack = new ArrayDeque<>();
stack.push(start);
while (!stack.isEmpty()) {
int node = stack.pop();
// A node can wait on the stack more than once. Visit only the first copy popped.
if (seen[node]) continue;
// Mark on pop, not on push. A node pushed early may be reached again from
// deeper down, and recursion would visit it from there.
seen[node] = true;
order.add(node);
// Push neighbors in reverse, so the first one comes off the stack first.
List<Integer> neighbors = graph.get(node);
for (int i = neighbors.size() - 1; i >= 0; i--) {
int neighbor = neighbors.get(i);
if (!seen[neighbor]) stack.push(neighbor);
}
}
return order;
}
// Fewest edges from start to every node, or -1 where no path exists.
static int[] bfsDistances(List<List<Integer>> graph, int start) {
int[] dist = new int[graph.size()];
Arrays.fill(dist, -1); // -1 also means "not seen yet".
dist[start] = 0;
ArrayDeque<Integer> queue = new ArrayDeque<>();
queue.add(start);
while (!queue.isEmpty()) {
int node = queue.poll();
for (int neighbor : graph.get(node)) {
if (dist[neighbor] == -1) {
// BFS reaches nodes in order of distance, so the first visit is the shortest.
dist[neighbor] = dist[node] + 1;
queue.add(neighbor);
}
}
}
return dist;
}
public static void main(String[] args) {
int[][] edges = {{0, 1}, {0, 3}, {1, 2}, {3, 4}, {2, 5}, {4, 5}};
List<List<Integer>> graph = buildGraph(7, edges);
System.out.println(bfsOrder(graph, 0)); // [0, 1, 3, 2, 4, 5]
System.out.println(dfsOrder(graph, 0)); // [0, 1, 2, 5, 4, 3]
System.out.println(dfsOrderIterative(graph, 0)); // [0, 1, 2, 5, 4, 3]
System.out.println(Arrays.toString(bfsDistances(graph, 0))); // [0, 1, 2, 1, 2, 3, -1]
}
}Worked Problems
Each solution stays folded until you open it, so you can try the problem first. Every one is a complete program in Python, TypeScript, Go, C# and Java, run and checked against its expected output.
Problem
You're given a grid of 1s (land) and 0s (water). An island is a group of land cells joined up, down, left or right. Cells that only touch diagonally aren't joined. Everything outside the grid is water. Return the number of islands. (LeetCode stores the cells as the characters "1" and "0". The logic is the same.)
Example: [[1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 1, 1]] returns 3: a 2 × 2 block in the top-left corner, a single cell in the middle, and two cells side by side in the bottom row.
Show SolutionHide Solution
Approach
Treat each land cell as a node. An island is then a connected component, so the answer is the number of components. Scan every cell. When you hit land that no earlier search has reached, you've found a new island: count it, then flood-fill the whole island so the scan never counts its cells again.
1 1 0 0 0 A A . . . 1 1 0 0 0 -> A A . . . 0 0 1 0 0 . . B . . 0 0 0 1 1 . . . C C
Counting Islands by Sinking Them
Step 1 of 13
grid
stack
- Current cell, from the scan or the stack
- Sunk and waiting on the stack
- Sunk by a flood fill
- Water
Scan the grid row by row, looking for land (1). Diagonal cells don't count as neighbors.
All 13 steps as text
- Scan the grid row by row, looking for land (
1). Diagonal cells don't count as neighbors. (0, 0)is land that no earlier search reached, so it's a new island:islands = 1. Sink it to0and push it, then flood the rest of the island.- Pop
(0, 0). Its land neighbors(1, 0)and(0, 1)are sunk as they're pushed, so no cell can enter the stack twice. - Pop
(0, 1). Its land neighbor(1, 1)is sunk as it's pushed. - Pop
(1, 1). Every neighbor is water, already sunk or off the grid, so nothing is pushed. - Pop
(1, 0). Every neighbor is water, already sunk or off the grid, so nothing is pushed. The stack is empty, so the whole island is sunk. - The scan walks past
(0, 1),(1, 0)and(1, 1)without counting them, because the first search sank them. It stops at(2, 2), which is still land. (2, 2)is land again, soislands = 2. Sink it and push it.- Pop
(2, 2). Every neighbor is water, already sunk or off the grid, so nothing is pushed. The stack is empty, so the whole island is sunk. (3, 3)is land again, soislands = 3. Sink it and push it.- Pop
(3, 3). Its land neighbor(3, 4)is sunk as it's pushed. - Pop
(3, 4). Every neighbor is water, already sunk or off the grid, so nothing is pushed. The stack is empty, so the whole island is sunk. - The scan passes
(3, 4), already sunk, and reaches the end. Each island was counted once, at its first cell, so the answer is3.
The flood fill is a DFS with an explicit stack, so a huge island can't overflow the call stack. It marks cells as visited by sinking them to 0, which saves a separate seen grid but changes the input. If the caller still needs the grid, copy it first or keep a seen array instead. BFS works just as well here, since the search visits every cell of the island either way.
Time O(R × C)Space O(R × C) for the stack when the grid is all land
DIRECTIONS = [(1, 0), (-1, 0), (0, 1), (0, -1)]
def num_islands(grid: list[list[int]]) -> int:
"""Number of islands in a grid of 1s (land) and 0s (water). Cells connect
up, down, left and right, never diagonally. Sinks the land it visits."""
islands = 0
for r in range(len(grid)):
for c in range(len(grid[0])):
if grid[r][c] == 1:
islands += 1 # Land no earlier search reached: a new island.
sink_island(grid, r, c)
return islands
def sink_island(grid: list[list[int]], r: int, c: int) -> None:
"""Iterative DFS: turn every land cell connected to (r, c) into water, so
the outer loop never counts this island again."""
rows, cols = len(grid), len(grid[0])
grid[r][c] = 0
stack = [(r, c)]
while stack:
row, col = stack.pop()
for dr, dc in DIRECTIONS:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 0 # Sink on push so no cell enters the stack twice.
stack.append((nr, nc))
if __name__ == "__main__":
print(num_islands([[1, 1, 1, 1, 0], [1, 1, 0, 1, 0], [1, 1, 0, 0, 0], [0, 0, 0, 0, 0]])) # 1
print(num_islands([[1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 1, 1]])) # 3
print(num_islands([[1, 0, 1], [0, 1, 0], [1, 0, 1]])) # 5const DIRECTIONS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
// Number of islands in a grid of 1s (land) and 0s (water). Cells connect
// up, down, left and right, never diagonally. Sinks the land it visits.
export function numIslands(grid: number[][]): number {
let islands = 0;
for (let r = 0; r < grid.length; r++) {
for (let c = 0; c < grid[0].length; c++) {
if (grid[r][c] === 1) {
islands++; // Land no earlier search reached: a new island.
sinkIsland(grid, r, c);
}
}
}
return islands;
}
// Iterative DFS: turn every land cell connected to (r, c) into water, so
// the outer loop never counts this island again.
function sinkIsland(grid: number[][], r: number, c: number): void {
const rows = grid.length;
const cols = grid[0].length;
grid[r][c] = 0;
const stack: [number, number][] = [[r, c]];
while (stack.length > 0) {
const [row, col] = stack.pop()!;
for (const [dr, dc] of DIRECTIONS) {
const nr = row + dr;
const nc = col + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
grid[nr][nc] = 0; // Sink on push so no cell enters the stack twice.
stack.push([nr, nc]);
}
}
}
}
console.log(numIslands([[1, 1, 1, 1, 0], [1, 1, 0, 1, 0], [1, 1, 0, 0, 0], [0, 0, 0, 0, 0]])); // 1
console.log(numIslands([[1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 1, 1]])); // 3
console.log(numIslands([[1, 0, 1], [0, 1, 0], [1, 0, 1]])); // 5package main
import "fmt"
var directions = [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
// numIslands returns the number of islands in a grid of 1s (land) and 0s
// (water). Cells connect up, down, left and right, never diagonally. It sinks
// the land it visits.
func numIslands(grid [][]int) int {
islands := 0
for r := range grid {
for c := range grid[0] {
if grid[r][c] == 1 {
islands++ // Land no earlier search reached: a new island.
sinkIsland(grid, r, c)
}
}
}
return islands
}
// sinkIsland is an iterative DFS: it turns every land cell connected to
// (r, c) into water, so the outer loop never counts this island again.
func sinkIsland(grid [][]int, r, c int) {
rows, cols := len(grid), len(grid[0])
grid[r][c] = 0
stack := [][2]int{{r, c}}
for len(stack) > 0 {
cell := stack[len(stack)-1]
stack = stack[:len(stack)-1]
row, col := cell[0], cell[1]
for _, d := range directions {
nr, nc := row+d[0], col+d[1]
if nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1 {
grid[nr][nc] = 0 // Sink on push so no cell enters the stack twice.
stack = append(stack, [2]int{nr, nc})
}
}
}
}
func main() {
fmt.Println(numIslands([][]int{{1, 1, 1, 1, 0}, {1, 1, 0, 1, 0}, {1, 1, 0, 0, 0}, {0, 0, 0, 0, 0}})) // 1
fmt.Println(numIslands([][]int{{1, 1, 0, 0, 0}, {1, 1, 0, 0, 0}, {0, 0, 1, 0, 0}, {0, 0, 0, 1, 1}})) // 3
fmt.Println(numIslands([][]int{{1, 0, 1}, {0, 1, 0}, {1, 0, 1}})) // 5
}using System;
using System.Collections.Generic;
public class Solution
{
static readonly (int Dr, int Dc)[] Directions = { (1, 0), (-1, 0), (0, 1), (0, -1) };
// Number of islands in a grid of 1s (land) and 0s (water). Cells connect
// up, down, left and right, never diagonally. Sinks the land it visits.
public static int NumIslands(int[][] grid)
{
int islands = 0;
for (int r = 0; r < grid.Length; r++)
{
for (int c = 0; c < grid[0].Length; c++)
{
if (grid[r][c] == 1)
{
islands++; // Land no earlier search reached: a new island.
SinkIsland(grid, r, c);
}
}
}
return islands;
}
// Iterative DFS: turn every land cell connected to (r, c) into water, so
// the outer loop never counts this island again.
static void SinkIsland(int[][] grid, int r, int c)
{
int rows = grid.Length, cols = grid[0].Length;
grid[r][c] = 0;
var stack = new Stack<(int, int)>();
stack.Push((r, c));
while (stack.Count > 0)
{
var (row, col) = stack.Pop();
foreach (var (dr, dc) in Directions)
{
int nr = row + dr, nc = col + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1)
{
grid[nr][nc] = 0; // Sink on push so no cell enters the stack twice.
stack.Push((nr, nc));
}
}
}
}
public static void Main()
{
Console.WriteLine(NumIslands([[1, 1, 1, 1, 0], [1, 1, 0, 1, 0], [1, 1, 0, 0, 0], [0, 0, 0, 0, 0]])); // 1
Console.WriteLine(NumIslands([[1, 1, 0, 0, 0], [1, 1, 0, 0, 0], [0, 0, 1, 0, 0], [0, 0, 0, 1, 1]])); // 3
Console.WriteLine(NumIslands([[1, 0, 1], [0, 1, 0], [1, 0, 1]])); // 5
}
}import java.util.ArrayDeque;
public class Solution {
static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// Number of islands in a grid of 1s (land) and 0s (water). Cells connect
// up, down, left and right, never diagonally. Sinks the land it visits.
static int numIslands(int[][] grid) {
int islands = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 1) {
islands++; // Land no earlier search reached: a new island.
sinkIsland(grid, r, c);
}
}
}
return islands;
}
// Iterative DFS: turn every land cell connected to (r, c) into water, so
// the outer loop never counts this island again.
static void sinkIsland(int[][] grid, int r, int c) {
int rows = grid.length, cols = grid[0].length;
grid[r][c] = 0;
ArrayDeque<int[]> stack = new ArrayDeque<>();
stack.push(new int[] {r, c});
while (!stack.isEmpty()) {
int[] cell = stack.pop();
int row = cell[0], col = cell[1];
for (int[] d : DIRECTIONS) {
int nr = row + d[0], nc = col + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
grid[nr][nc] = 0; // Sink on push so no cell enters the stack twice.
stack.push(new int[] {nr, nc});
}
}
}
}
public static void main(String[] args) {
System.out.println(numIslands(new int[][] {{1, 1, 1, 1, 0}, {1, 1, 0, 1, 0}, {1, 1, 0, 0, 0}, {0, 0, 0, 0, 0}})); // 1
System.out.println(numIslands(new int[][] {{1, 1, 0, 0, 0}, {1, 1, 0, 0, 0}, {0, 0, 1, 0, 0}, {0, 0, 0, 1, 1}})); // 3
System.out.println(numIslands(new int[][] {{1, 0, 1}, {0, 1, 0}, {1, 0, 1}})); // 5
}
}Problem
Each cell of a grid is empty (0), holds a fresh orange (1) or holds a rotten orange (2). Every minute, each fresh orange directly above, below, left or right of a rotten one turns rotten. Return the number of minutes until no fresh orange is left, or -1 if some fresh orange can never rot.
Example: [[2, 1, 1], [1, 1, 0], [0, 1, 1]] returns 4. The rot spreads from the top-left corner and reaches the bottom-right orange last, in minute 4.
Show SolutionHide Solution
Approach
Rot that spreads one step per minute is BFS: an orange rots in minute m exactly when its nearest rotten orange is m steps away, stepping only through oranges. The twist is that several oranges can be rotten at the start, and they all spread at once. So seed the queue with every rotten orange before the first step. This multi-source BFS behaves as if one imaginary node were joined to all the sources and the search started there.
grid minute it rots 2 1 1 0 1 2 1 1 0 1 2 . 0 1 1 . 3 4
Run the search one frontier at a time. Every orange the current frontier rots goes into the next frontier, and each pass is one minute. Count the fresh oranges up front and subtract as they rot. If the frontier runs dry while some are still fresh, those oranges are walled off from every rotten one, so the answer is -1. The loop also stops as soon as nothing fresh is left, which makes a grid with no fresh oranges take 0 minutes. Like Number of Islands, it marks progress by rewriting the grid in place, so copy the grid first if the caller still needs it.
A separate BFS from each rotten orange would also work, but it costs O(k × R × C) for k rotten oranges, and you'd still have to combine the results. One multi-source pass is O(R × C). The last demo, [[2, 1, 1, 1, 2]], shows why the sources must start together: it takes 2 minutes, not the 3 it would take if rot spread from one end only, because both ends rot toward the middle at the same time.
Time O(R × C)Space O(R × C)
DIRECTIONS = [(1, 0), (-1, 0), (0, 1), (0, -1)]
def oranges_rotting(grid: list[list[int]]) -> int:
"""Minutes until no fresh orange (1) is left when every rotten orange (2)
rots its four neighbors each minute, or -1 if some orange never rots."""
rows, cols = len(grid), len(grid[0])
frontier = []
fresh = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == 2:
frontier.append((r, c)) # Every rotten orange starts the BFS.
elif grid[r][c] == 1:
fresh += 1
minutes = 0
# Each pass rots everything next to the current frontier: one minute.
while frontier and fresh > 0:
newly_rotten = []
for row, col in frontier:
for dr, dc in DIRECTIONS:
nr, nc = row + dr, col + dc
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] == 1:
grid[nr][nc] = 2 # Rot it now so no other neighbor counts it again.
fresh -= 1
newly_rotten.append((nr, nc))
frontier = newly_rotten
minutes += 1
return minutes if fresh == 0 else -1
if __name__ == "__main__":
print(oranges_rotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]])) # 4
print(oranges_rotting([[2, 1, 1], [0, 1, 1], [1, 0, 1]])) # -1
print(oranges_rotting([[0, 2]])) # 0
print(oranges_rotting([[2, 1, 1, 1, 2]])) # 2const DIRECTIONS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
// Minutes until no fresh orange (1) is left when every rotten orange (2)
// rots its four neighbors each minute, or -1 if some orange never rots.
export function orangesRotting(grid: number[][]): number {
const rows = grid.length;
const cols = grid[0].length;
let frontier: [number, number][] = [];
let fresh = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (grid[r][c] === 2) frontier.push([r, c]); // Every rotten orange starts the BFS.
else if (grid[r][c] === 1) fresh++;
}
}
let minutes = 0;
// Each pass rots everything next to the current frontier: one minute.
while (frontier.length > 0 && fresh > 0) {
const newlyRotten: [number, number][] = [];
for (const [row, col] of frontier) {
for (const [dr, dc] of DIRECTIONS) {
const nr = row + dr;
const nc = col + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] === 1) {
grid[nr][nc] = 2; // Rot it now so no other neighbor counts it again.
fresh--;
newlyRotten.push([nr, nc]);
}
}
}
frontier = newlyRotten;
minutes++;
}
return fresh === 0 ? minutes : -1;
}
console.log(orangesRotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]])); // 4
console.log(orangesRotting([[2, 1, 1], [0, 1, 1], [1, 0, 1]])); // -1
console.log(orangesRotting([[0, 2]])); // 0
console.log(orangesRotting([[2, 1, 1, 1, 2]])); // 2package main
import "fmt"
var directions = [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
// orangesRotting returns the minutes until no fresh orange (1) is left when
// every rotten orange (2) rots its four neighbors each minute, or -1 if some
// orange never rots.
func orangesRotting(grid [][]int) int {
rows, cols := len(grid), len(grid[0])
frontier := [][2]int{}
fresh := 0
for r := 0; r < rows; r++ {
for c := 0; c < cols; c++ {
if grid[r][c] == 2 {
frontier = append(frontier, [2]int{r, c}) // Every rotten orange starts the BFS.
} else if grid[r][c] == 1 {
fresh++
}
}
}
minutes := 0
// Each pass rots everything next to the current frontier: one minute.
for len(frontier) > 0 && fresh > 0 {
newlyRotten := [][2]int{}
for _, cell := range frontier {
for _, d := range directions {
nr, nc := cell[0]+d[0], cell[1]+d[1]
if nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1 {
grid[nr][nc] = 2 // Rot it now so no other neighbor counts it again.
fresh--
newlyRotten = append(newlyRotten, [2]int{nr, nc})
}
}
}
frontier = newlyRotten
minutes++
}
if fresh > 0 {
return -1
}
return minutes
}
func main() {
fmt.Println(orangesRotting([][]int{{2, 1, 1}, {1, 1, 0}, {0, 1, 1}})) // 4
fmt.Println(orangesRotting([][]int{{2, 1, 1}, {0, 1, 1}, {1, 0, 1}})) // -1
fmt.Println(orangesRotting([][]int{{0, 2}})) // 0
fmt.Println(orangesRotting([][]int{{2, 1, 1, 1, 2}})) // 2
}using System;
using System.Collections.Generic;
public class Solution
{
static readonly (int Dr, int Dc)[] Directions = { (1, 0), (-1, 0), (0, 1), (0, -1) };
// Minutes until no fresh orange (1) is left when every rotten orange (2)
// rots its four neighbors each minute, or -1 if some orange never rots.
public static int OrangesRotting(int[][] grid)
{
int rows = grid.Length, cols = grid[0].Length;
var frontier = new List<(int, int)>();
int fresh = 0;
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < cols; c++)
{
if (grid[r][c] == 2) frontier.Add((r, c)); // Every rotten orange starts the BFS.
else if (grid[r][c] == 1) fresh++;
}
}
int minutes = 0;
// Each pass rots everything next to the current frontier: one minute.
while (frontier.Count > 0 && fresh > 0)
{
var newlyRotten = new List<(int, int)>();
foreach (var (row, col) in frontier)
{
foreach (var (dr, dc) in Directions)
{
int nr = row + dr, nc = col + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1)
{
grid[nr][nc] = 2; // Rot it now so no other neighbor counts it again.
fresh--;
newlyRotten.Add((nr, nc));
}
}
}
frontier = newlyRotten;
minutes++;
}
return fresh == 0 ? minutes : -1;
}
public static void Main()
{
Console.WriteLine(OrangesRotting([[2, 1, 1], [1, 1, 0], [0, 1, 1]])); // 4
Console.WriteLine(OrangesRotting([[2, 1, 1], [0, 1, 1], [1, 0, 1]])); // -1
Console.WriteLine(OrangesRotting([[0, 2]])); // 0
Console.WriteLine(OrangesRotting([[2, 1, 1, 1, 2]])); // 2
}
}import java.util.ArrayList;
import java.util.List;
public class Solution {
static final int[][] DIRECTIONS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// Minutes until no fresh orange (1) is left when every rotten orange (2)
// rots its four neighbors each minute, or -1 if some orange never rots.
static int orangesRotting(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
List<int[]> frontier = new ArrayList<>();
int fresh = 0;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
if (grid[r][c] == 2) frontier.add(new int[] {r, c}); // Every rotten orange starts the BFS.
else if (grid[r][c] == 1) fresh++;
}
}
int minutes = 0;
// Each pass rots everything next to the current frontier: one minute.
while (!frontier.isEmpty() && fresh > 0) {
List<int[]> newlyRotten = new ArrayList<>();
for (int[] cell : frontier) {
for (int[] d : DIRECTIONS) {
int nr = cell[0] + d[0], nc = cell[1] + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
grid[nr][nc] = 2; // Rot it now so no other neighbor counts it again.
fresh--;
newlyRotten.add(new int[] {nr, nc});
}
}
}
frontier = newlyRotten;
minutes++;
}
return fresh == 0 ? minutes : -1;
}
public static void main(String[] args) {
System.out.println(orangesRotting(new int[][] {{2, 1, 1}, {1, 1, 0}, {0, 1, 1}})); // 4
System.out.println(orangesRotting(new int[][] {{2, 1, 1}, {0, 1, 1}, {1, 0, 1}})); // -1
System.out.println(orangesRotting(new int[][] {{0, 2}})); // 0
System.out.println(orangesRotting(new int[][] {{2, 1, 1, 1, 2}})); // 2
}
}Common Pitfalls
- Marking a node as seen when it leaves the queue instead of when it joins it. Two neighbors can both queue the same node before it's processed, so it gets visited twice.
- Storing an undirected edge in only one direction. Part of the graph then can't reach the rest, and the search quietly misses it.
- Using DFS to find the fewest steps. DFS reaches a node by whichever path it tries first, not the shortest one. Use BFS.
- Recursing over a large grid or a long chain. The depth can reach V and overflow the call stack, so switch to an explicit stack.
- Starting one search from node 0 when the graph might be disconnected. Loop over every node and start a new search wherever you find one that hasn't been seen.
Practice
More problems to try on your own.