Depth-First Search (DFS) Algorithm

Learn Depth-First Search (DFS) Algorithm with practical examples, explanations, best practices, and real-world coding techniques.

What Is Depth-First Search (DFS)?

Depth-First Search (DFS) is a graph traversal algorithm that explores a path as deeply as possible before returning to explore another path. Instead of visiting all nearby vertices first, DFS follows one neighbor, then continues through that neighbor's neighbors until it reaches a point where it cannot continue.

DFS can be implemented using recursion or an explicit stack. Both approaches follow the same depth-first idea.

Core idea: Start at a vertex, visit an unvisited neighbor, continue deeper until there are no unvisited neighbors, then backtrack and explore the next available path.

How DFS Traverses a Graph

Consider the following graph:

        A
       / \
      B   C
     / \
    D   E

If DFS starts at A and neighbors are processed in the order shown, one possible traversal is:

A → B → D → E → C

DFS first moves from A to B, then goes deeper to D. After D has no unvisited neighbors, DFS backtracks to B and explores E. It eventually returns to A and explores C.

DFS and Backtracking

Backtracking is one of the most important ideas behind DFS. When the current vertex has no unvisited neighbor, the algorithm returns to the previous vertex and checks whether another path is available.

A
|
B
|
D

D has no unvisited neighbor
        ↓
Backtrack to B
        ↓
Explore another neighbor of B

This depth-and-backtrack behavior also appears in maze solving, tree traversal, dependency analysis, connected-component algorithms, and many recursive problem-solving techniques.

DFS Using Recursion

Recursive DFS uses the JavaScript call stack as its implicit stack. When a vertex is visited, the function recursively explores each unvisited neighbor.

function dfs(graph, node, visited = new Set()) {
  if (visited.has(node)) {
    return;
  }

  visited.add(node);
  console.log(node);

  for (const neighbor of graph.get(node) || []) {
    dfs(graph, neighbor, visited);
  }
}

const graph = new Map([
  ['A', ['B', 'C']],
  ['B', ['A', 'D', 'E']],
  ['C', ['A']],
  ['D', ['B']],
  ['E', ['B']]
]);

dfs(graph, 'A');

// A
// B
// D
// E
// C

The visited set prevents DFS from repeatedly traversing vertices when the graph contains cycles.

DFS Using an Explicit Stack

DFS does not require recursion. An explicit stack can be used to make the traversal iterative. This approach is useful when you want direct control over the stack or want to avoid very deep recursive call chains.

function dfsIterative(graph, start) {
  const stack = [start];
  const visited = new Set();
  const result = [];

  while (stack.length > 0) {
    const current = stack.pop();

    if (visited.has(current)) {
      continue;
    }

    visited.add(current);
    result.push(current);

    const neighbors = graph.get(current) || [];

    for (let i = neighbors.length - 1; i >= 0; i--) {
      if (!visited.has(neighbors[i])) {
        stack.push(neighbors[i]);
      }
    }
  }

  return result;
}

const graph = new Map([
  ['A', ['B', 'C']],
  ['B', ['A', 'D', 'E']],
  ['C', ['A']],
  ['D', ['B']],
  ['E', ['B']]
]);

console.log(dfsIterative(graph, 'A'));
// ['A', 'B', 'D', 'E', 'C']

The neighbors are pushed in reverse order so that the stack processes them in the same left-to-right order as the recursive example. The exact traversal order is not universal; it depends on how neighbors are stored and processed.

Why Does DFS Need a Visited Set?

Graphs can contain cycles, meaning a path can eventually lead back to a vertex that has already been visited.

A → B
↑   ↓
└── C

Without a visited structure, DFS could keep following the cycle: A → B → C → A → B → C.

The visited set records vertices that have already been discovered, allowing DFS to stop revisiting them.

DFS with an Adjacency List

An adjacency list is one of the most convenient graph representations for DFS because the algorithm can directly iterate through the neighbors of the current vertex.

const graph = {
  A: ['B', 'C'],
  B: ['A', 'D'],
  C: ['A', 'E'],
  D: ['B'],
  E: ['B', 'C']
};

function dfs(graph, node, visited = new Set(), result = []) {
  if (visited.has(node)) {
    return result;
  }

  visited.add(node);
  result.push(node);

  for (const neighbor of graph[node] || []) {
    dfs(graph, neighbor, visited, result);
  }

  return result;
}

console.log(dfs(graph, 'A'));
// ['A', 'B', 'D', 'E', 'C']

DFS with an Adjacency Matrix

DFS can also work with an adjacency matrix. Instead of iterating through a neighbor list, the algorithm checks each possible vertex to determine whether an edge exists.

const matrix = [
  [0, 1, 1, 0],
  [1, 0, 0, 1],
  [1, 0, 0, 0],
  [0, 1, 0, 0]
];

function dfsMatrix(matrix, start, visited = new Set(), result = []) {
  visited.add(start);
  result.push(start);

  for (let neighbor = 0; neighbor < matrix.length; neighbor++) {
    if (matrix[start][neighbor] === 1 && !visited.has(neighbor)) {
      dfsMatrix(matrix, neighbor, visited, result);
    }
  }

  return result;
}

console.log(dfsMatrix(matrix, 0));
// [0, 1, 3, 2]

With an adjacency matrix, checking all possible neighbors requires O(V) work for each processed vertex. This is one reason adjacency lists are often preferred for sparse graphs.

DFS for Searching a Target

DFS can stop as soon as a target vertex is found. This is useful when the goal is simply to determine whether a vertex is reachable from another vertex.

function dfsSearch(graph, current, target, visited = new Set()) {
  if (current === target) {
    return true;
  }

  visited.add(current);

  for (const neighbor of graph.get(current) || []) {
    if (!visited.has(neighbor)) {
      if (dfsSearch(graph, neighbor, target, visited)) {
        return true;
      }
    }
  }

  return false;
}

const graph = new Map([
  ['A', ['B', 'C']],
  ['B', ['D']],
  ['C', ['E']],
  ['D', []],
  ['E', []]
]);

console.log(dfsSearch(graph, 'A', 'E'));
// true

console.log(dfsSearch(graph, 'A', 'X'));
// false

DFS for Finding a Path

DFS can record the previous vertex while exploring the graph. If the target is found, the stored parent information can be used to reconstruct a path.

function dfsPath(graph, start, target) {
  const visited = new Set();
  const previous = new Map();

  function search(current) {
    if (visited.has(current)) {
      return false;
    }

    visited.add(current);

    if (current === target) {
      return true;
    }

    for (const neighbor of graph.get(current) || []) {
      if (!visited.has(neighbor)) {
        previous.set(neighbor, current);

        if (search(neighbor)) {
          return true;
        }
      }
    }

    return false;
  }

  if (!search(start)) {
    return [];
  }

  const path = [];
  let current = target;

  while (current !== undefined) {
    path.push(current);
    current = previous.get(current);
  }

  return path.reverse();
}

const graph = new Map([
  ['A', ['B', 'C']],
  ['B', ['D']],
  ['C', ['E']],
  ['D', ['F']],
  ['E', ['F']],
  ['F', []]
]);

console.log(dfsPath(graph, 'A', 'F'));
// ['A', 'B', 'D', 'F']

DFS can find a path, but the path it finds is not necessarily the shortest path in an unweighted graph. If the problem specifically asks for the minimum number of edges or minimum moves, BFS is generally the appropriate traversal.

DFS for Connected Components

DFS can be used to find connected components in an undirected graph. Start DFS from every vertex that has not yet been visited. Each DFS traversal discovers one complete component.

function countComponents(graph) {
  const visited = new Set();
  let count = 0;

  function dfs(node) {
    if (visited.has(node)) {
      return;
    }

    visited.add(node);

    for (const neighbor of graph.get(node) || []) {
      dfs(neighbor);
    }
  }

  for (const node of graph.keys()) {
    if (!visited.has(node)) {
      count++;
      dfs(node);
    }
  }

  return count;
}

const graph = new Map([
  ['A', ['B']],
  ['B', ['A']],
  ['C', ['D']],
  ['D', ['C']],
  ['E', []]
]);

console.log(countComponents(graph));
// 3

DFS for Cycle Detection

DFS is commonly used to detect cycles. The exact technique depends on whether the graph is directed or undirected.

In a directed graph, a common approach tracks vertices currently present in the recursion path. Encountering a vertex that is already in that active path indicates a back edge and therefore a cycle.

function hasCycle(graph) {
  const visited = new Set();
  const visiting = new Set();

  function dfs(node) {
    if (visiting.has(node)) {
      return true;
    }

    if (visited.has(node)) {
      return false;
    }

    visiting.add(node);

    for (const neighbor of graph.get(node) || []) {
      if (dfs(neighbor)) {
        return true;
      }
    }

    visiting.delete(node);
    visited.add(node);

    return false;
  }

  for (const node of graph.keys()) {
    if (dfs(node)) {
      return true;
    }
  }

  return false;
}

const graph = new Map([
  ['A', ['B']],
  ['B', ['C']],
  ['C', ['A']]
]);

console.log(hasCycle(graph));
// true

DFS in Trees

DFS is also fundamental to tree traversal. A binary tree can be explored using preorder, inorder, or postorder traversal.

        1
       / \
      2   3
     / \
    4   5

Preorder:  1 → 2 → 4 → 5 → 3
Inorder:   4 → 2 → 5 → 1 → 3
Postorder: 4 → 5 → 2 → 3 → 1

These traversals are DFS patterns because the algorithm explores a branch before moving back to another branch.

DFS Preorder Traversal

In preorder traversal, the current node is processed before its children.

function preorder(node, result = []) {
  if (node === null) {
    return result;
  }

  result.push(node.value);
  preorder(node.left, result);
  preorder(node.right, result);

  return result;
}

DFS Postorder Traversal

In postorder traversal, child nodes are processed before the current node. This pattern is useful when a parent depends on work being completed for its children first.

function postorder(node, result = []) {
  if (node === null) {
    return result;
  }

  postorder(node.left, result);
  postorder(node.right, result);
  result.push(node.value);

  return result;
}

DFS and Topological Sorting

DFS can be used to produce a topological ordering of a directed acyclic graph (DAG). A common approach is to perform DFS and add a vertex to a result stack after all of its outgoing neighbors have been processed.

function topologicalSort(graph) {
  const visited = new Set();
  const result = [];

  function dfs(node) {
    if (visited.has(node)) {
      return;
    }

    visited.add(node);

    for (const neighbor of graph.get(node) || []) {
      dfs(neighbor);
    }

    result.push(node);
  }

  for (const node of graph.keys()) {
    dfs(node);
  }

  return result.reverse();
}

const graph = new Map([
  ['Learn', ['Practice']],
  ['Practice', ['Build']],
  ['Build', []]
]);

console.log(topologicalSort(graph));
// ['Learn', 'Practice', 'Build']

A production implementation of topological sorting should also detect cycles because a directed graph containing a cycle does not have a valid topological ordering.

DFS Time and Space Complexity

With an adjacency list, DFS visits each reachable vertex and examines each relevant edge.

  • Time complexity: O(V + E)
  • Space complexity: O(V)

Here, V is the number of vertices and E is the number of edges. The O(V) space can come from the visited set and the DFS stack, whether that stack is explicit or represented by the recursive call stack.

With an adjacency matrix, DFS may spend O(V) time checking possible neighbors for every vertex, resulting in O(V²) traversal time.

BFS vs DFS

BFS and DFS both traverse graphs, but their exploration strategies are different.

Feature DFS BFS
Main structure Stack or recursion Queue
Traversal style Deep before wide Level by level
Shortest path in unweighted graph Not guaranteed Yes
Backtracking Natural fit Not the primary pattern
Common uses Cycle detection, components, topological sorting Shortest paths, levels, minimum moves

When Should You Use DFS?

DFS is useful when the problem benefits from exploring an entire branch before moving to another branch.

  • Traversing trees and hierarchical structures.
  • Detecting cycles in graphs.
  • Finding connected components.
  • Checking whether a path exists between vertices.
  • Topological sorting of directed acyclic graphs.
  • Backtracking and state-space exploration.
  • Maze and grid exploration.
  • Dependency and prerequisite analysis.
  • Exploring nested or hierarchical data.

DFS in Grid Problems

A grid can be modeled as a graph where each walkable cell is a vertex and neighboring cells are connected. DFS can explore an entire region of connected cells.

This makes DFS useful for problems such as counting islands, exploring rooms, identifying connected regions, and checking whether cells are reachable.

const grid = [
  ['1', '1', '0'],
  ['1', '0', '0'],
  ['0', '0', '1']
];

function dfs(grid, row, col) {
  if (
    row < 0 ||
    row >= grid.length ||
    col < 0 ||
    col >= grid[0].length ||
    grid[row][col] !== '1'
  ) {
    return;
  }

  grid[row][col] = '0';

  dfs(grid, row + 1, col);
  dfs(grid, row - 1, col);
  dfs(grid, row, col + 1);
  dfs(grid, row, col - 1);
}

dfs(grid, 0, 0);

console.log(grid);
// Connected cells from the starting position have been explored.

Recursive DFS vs Iterative DFS

Both implementations perform depth-first traversal, but they manage traversal state differently.

Aspect Recursive DFS Iterative DFS
Stack Call stack Explicit stack
Code style Usually concise More explicit
Deep graphs Can encounter call-stack limits Avoids recursive call depth
Backtracking Natural Managed explicitly

Common DFS Mistakes

1. Forgetting to Track Visited Vertices

A graph can contain cycles, so DFS needs a way to identify vertices that have already been explored.

2. Assuming DFS Finds the Shortest Path

DFS may find a valid path without finding the path with the fewest edges. Use BFS when the problem asks for the shortest path in an unweighted graph.

3. Ignoring Neighbor Order

DFS traversal order depends on the order in which neighbors are processed. More than one traversal can therefore be valid for the same graph.

4. Using Deep Recursion Without Considering Stack Limits

Very deep graphs can produce a large recursive call stack. An iterative implementation with an explicit stack can be preferable for such cases.

5. Confusing Graph DFS with Tree DFS

Tree traversal often relies on the tree structure itself, while a general graph may contain cycles and multiple paths to the same vertex. Graph DFS therefore normally requires explicit visited tracking.

DFS Interview Pattern

DFS is often a useful first consideration when an interview problem involves exploring all possibilities, connected regions, recursive relationships, or graph structure.

  • Ask whether the problem can be modeled as a graph or tree.
  • Identify the starting vertex or state.
  • Determine how neighboring states are generated.
  • Decide whether visited tracking is required.
  • Choose recursion or an explicit stack.
  • Return or record information while exploring and backtracking.

Words such as explore, connected, reachability, all possible paths, cycle, and dependencies can be useful clues that DFS may fit the problem.

Key Takeaways

  • DFS explores a graph deeply before backtracking.
  • DFS can be implemented recursively or with an explicit stack.
  • A visited set is important when traversing general graphs with cycles.
  • DFS works naturally with tree and hierarchical structures.
  • DFS is useful for connected components, cycle detection, path existence, and topological sorting.
  • DFS does not guarantee the shortest path in an unweighted graph.
  • With an adjacency list, DFS runs in O(V + E) time.
  • The exact DFS traversal order depends on neighbor ordering.

Code Examples

function dfs(graph, node, visited = new Set()) {
  if (visited.has(node)) {
    return;
  }

  visited.add(node);
  console.log(node);

  for (const neighbor of graph.get(node) || []) {
    dfs(graph, neighbor, visited);
  }
}

const graph = new Map([
  ['A', ['B', 'C']],
  ['B', ['A', 'D', 'E']],
  ['C', ['A']],
  ['D', ['B']],
  ['E', ['B']]
]);

dfs(graph, 'A');

// A
// B
// D
// E
// C
    
function dfsIterative(graph, start) {
  const stack = [start];
  const visited = new Set();
  const result = [];

  while (stack.length > 0) {
    const current = stack.pop();

    if (visited.has(current)) {
      continue;
    }

    visited.add(current);
    result.push(current);

    const neighbors = graph.get(current) || [];

    for (let i = neighbors.length - 1; i >= 0; i--) {
      if (!visited.has(neighbors[i])) {
        stack.push(neighbors[i]);
      }
    }
  }

  return result;
}

const graph = new Map([
  ['A', ['B', 'C']],
  ['B', ['A', 'D', 'E']],
  ['C', ['A']],
  ['D', ['B']],
  ['E', ['B']]
]);

console.log(dfsIterative(graph, 'A'));
// ['A', 'B', 'D', 'E', 'C']
    
function dfsSearch(graph, current, target, visited = new Set()) {
  if (current === target) {
    return true;
  }

  visited.add(current);

  for (const neighbor of graph.get(current) || []) {
    if (!visited.has(neighbor)) {
      if (dfsSearch(graph, neighbor, target, visited)) {
        return true;
      }
    }
  }

  return false;
}

const graph = new Map([
  ['A', ['B', 'C']],
  ['B', ['D']],
  ['C', ['E']],
  ['D', []],
  ['E', []]
]);

console.log(dfsSearch(graph, 'A', 'E'));
// true
    

Pro Tip

When a graph problem asks you to explore connected regions, detect cycles, check reachability, or investigate possibilities deeply, DFS is often a useful pattern to consider. For very deep graphs, prefer an explicit stack when recursive call depth could become a limitation.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.