Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking.
In simpler terms, DFS explores a path all the way to its end before moving on to the next path, going deep before going wide.
Here's a simple recursive implementation of DFS for a graph represented as an adjacency list:
Here's an iterative implementation of DFS using a stack:
O(V + E), where V is the number of vertices and E is the number of edges in the graph.
We visit each vertex once and each edge once.
O(V), where V is the number of vertices in the graph.
In the worst case, the stack might contain all vertices of the graph. For recursive implementation, the call stack might grow to size V.
Understand the fundamental concepts of depth-first search and its importance in graph traversal, backtracking, and cycle detection.
Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking.
In simpler terms, DFS explores a path all the way to its end before moving on to the next path, going deep before going wide.
Think of DFS as exploring a maze by following a single path until you hit a dead end, then backtracking to the last intersection and trying a different path.
Uses a stack data structure (or recursion) to keep track of vertices to visit, following a Last In, First Out (LIFO) approach.
Explores as far as possible along each branch before backtracking, creating a depth-wise exploration.
Requires less memory than breadth-first search because it only needs to store the vertices on the current path.
Does not guarantee the shortest path in unweighted graphs, as it might explore a long path first.
Begin at a source vertex, mark it as visited, and process it (e.g., add to result).
Choose one of its unvisited neighbors and recursively apply DFS to that neighbor.
Continue this process, always exploring an unvisited neighbor of the most recently visited vertex.
When a vertex has no unvisited neighbors, backtrack to the previous vertex and explore its other neighbors.
Repeat until all vertices reachable from the source are visited.
Here's a recursive implementation of DFS for a graph represented as an adjacency list:
12345678910111213141516171819202122232425262728293031323334353637383940414243function dfs(graph, startVertex) { // Set to keep track of visited vertices const visited = new Set(); // Result array to store the DFS traversal order const result = []; // Recursive helper function function dfsHelper(vertex) { // Mark the vertex as visited visited.add(vertex); // Process the vertex (add to result) result.push(vertex); // Explore all adjacent vertices for (const neighbor of graph[vertex]) { // If this neighbor has not been visited yet if (!visited.has(neighbor)) { // Recursively visit the neighbor dfsHelper(neighbor); } } } // Start DFS from the start vertex dfsHelper(startVertex); return result;} // Example usage:const graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E']}; const traversalOrder = dfs(graph, 'A');console.log(traversalOrder); // Output: ['A', 'B', 'D', 'E', 'F', 'C']
Note: The recursive implementation is elegant and intuitive, but it may cause a stack overflow for very deep graphs. In such cases, the iterative implementation is preferred.
Here's an iterative implementation of DFS using a stack:
123456789101112131415161718192021222324252627282930313233343536373839function dfsIterative(graph, startVertex) { // Set to keep track of visited vertices const visited = new Set(); // Stack for DFS (using array as stack) const stack = [startVertex]; // Result array to store the DFS traversal order const result = []; // Mark the start vertex as visited visited.add(startVertex); // While there are vertices in the stack while (stack.length > 0) { // Pop a vertex from the stack const currentVertex = stack.pop(); // Process the vertex (add to result) result.push(currentVertex); // Get all adjacent vertices of the popped vertex // Add unvisited neighbors to the stack in reverse order // (to match the recursive implementation) for (const neighbor of [...graph[currentVertex]].reverse()) { if (!visited.has(neighbor)) { // Mark it as visited and push it to the stack visited.add(neighbor); stack.push(neighbor); } } } return result;} // Example usage:const traversalOrderIterative = dfsIterative(graph, 'A');console.log(traversalOrderIterative); // Output: ['A', 'B', 'D', 'E', 'F', 'C']
Key Insight: The iterative implementation explicitly uses a stack to mimic the call stack of the recursive implementation. Note that we process neighbors in reverse order to match the recursive implementation's traversal order.
O(V + E), where V is the number of vertices and E is the number of edges in the graph.
Explanation: In the worst case, we visit each vertex once (O(V)) and explore each edge once (O(E)). For a connected graph, E is at least V-1, so the time complexity is dominated by the number of edges.
O(V), where V is the number of vertices in the graph.
Explanation: We need to store the visited set (O(V)) and the stack (O(V) in the worst case). For the recursive implementation, the call stack might grow to size V in the worst case. Overall, the space complexity is O(V).
Both DFS and BFS have the same time complexity (O(V + E)), but DFS typically requires less memory because it only needs to store the vertices on the current path, whereas BFS needs to store all vertices at the current level.
DFS is the backbone of backtracking algorithms, which are used to solve puzzles, generate mazes, and solve combinatorial problems like N-Queens and Sudoku.
DFS can be used to detect cycles in a graph by keeping track of vertices in the current recursion stack.
DFS can be used to perform a topological sort of a directed acyclic graph (DAG), which is useful for scheduling tasks with dependencies.
DFS can be used to find all connected components in an undirected graph and strongly connected components in a directed graph.
Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking.
In simpler terms, DFS explores a path all the way to its end before moving on to the next path, going deep before going wide.
Here's a simple recursive implementation of DFS for a graph represented as an adjacency list:
Here's an iterative implementation of DFS using a stack:
O(V + E), where V is the number of vertices and E is the number of edges in the graph.
We visit each vertex once and each edge once.
O(V), where V is the number of vertices in the graph.
In the worst case, the stack might contain all vertices of the graph. For recursive implementation, the call stack might grow to size V.
Understand the fundamental concepts of depth-first search and its importance in graph traversal, backtracking, and cycle detection.
Depth-First Search (DFS) is a graph traversal algorithm that explores as far as possible along each branch before backtracking.
In simpler terms, DFS explores a path all the way to its end before moving on to the next path, going deep before going wide.
Think of DFS as exploring a maze by following a single path until you hit a dead end, then backtracking to the last intersection and trying a different path.
Uses a stack data structure (or recursion) to keep track of vertices to visit, following a Last In, First Out (LIFO) approach.
Explores as far as possible along each branch before backtracking, creating a depth-wise exploration.
Requires less memory than breadth-first search because it only needs to store the vertices on the current path.
Does not guarantee the shortest path in unweighted graphs, as it might explore a long path first.
Begin at a source vertex, mark it as visited, and process it (e.g., add to result).
Choose one of its unvisited neighbors and recursively apply DFS to that neighbor.
Continue this process, always exploring an unvisited neighbor of the most recently visited vertex.
When a vertex has no unvisited neighbors, backtrack to the previous vertex and explore its other neighbors.
Repeat until all vertices reachable from the source are visited.
Here's a recursive implementation of DFS for a graph represented as an adjacency list:
12345678910111213141516171819202122232425262728293031323334353637383940414243function dfs(graph, startVertex) { // Set to keep track of visited vertices const visited = new Set(); // Result array to store the DFS traversal order const result = []; // Recursive helper function function dfsHelper(vertex) { // Mark the vertex as visited visited.add(vertex); // Process the vertex (add to result) result.push(vertex); // Explore all adjacent vertices for (const neighbor of graph[vertex]) { // If this neighbor has not been visited yet if (!visited.has(neighbor)) { // Recursively visit the neighbor dfsHelper(neighbor); } } } // Start DFS from the start vertex dfsHelper(startVertex); return result;} // Example usage:const graph = { 'A': ['B', 'C'], 'B': ['A', 'D', 'E'], 'C': ['A', 'F'], 'D': ['B'], 'E': ['B', 'F'], 'F': ['C', 'E']}; const traversalOrder = dfs(graph, 'A');console.log(traversalOrder); // Output: ['A', 'B', 'D', 'E', 'F', 'C']
Note: The recursive implementation is elegant and intuitive, but it may cause a stack overflow for very deep graphs. In such cases, the iterative implementation is preferred.
Here's an iterative implementation of DFS using a stack:
123456789101112131415161718192021222324252627282930313233343536373839function dfsIterative(graph, startVertex) { // Set to keep track of visited vertices const visited = new Set(); // Stack for DFS (using array as stack) const stack = [startVertex]; // Result array to store the DFS traversal order const result = []; // Mark the start vertex as visited visited.add(startVertex); // While there are vertices in the stack while (stack.length > 0) { // Pop a vertex from the stack const currentVertex = stack.pop(); // Process the vertex (add to result) result.push(currentVertex); // Get all adjacent vertices of the popped vertex // Add unvisited neighbors to the stack in reverse order // (to match the recursive implementation) for (const neighbor of [...graph[currentVertex]].reverse()) { if (!visited.has(neighbor)) { // Mark it as visited and push it to the stack visited.add(neighbor); stack.push(neighbor); } } } return result;} // Example usage:const traversalOrderIterative = dfsIterative(graph, 'A');console.log(traversalOrderIterative); // Output: ['A', 'B', 'D', 'E', 'F', 'C']
Key Insight: The iterative implementation explicitly uses a stack to mimic the call stack of the recursive implementation. Note that we process neighbors in reverse order to match the recursive implementation's traversal order.
O(V + E), where V is the number of vertices and E is the number of edges in the graph.
Explanation: In the worst case, we visit each vertex once (O(V)) and explore each edge once (O(E)). For a connected graph, E is at least V-1, so the time complexity is dominated by the number of edges.
O(V), where V is the number of vertices in the graph.
Explanation: We need to store the visited set (O(V)) and the stack (O(V) in the worst case). For the recursive implementation, the call stack might grow to size V in the worst case. Overall, the space complexity is O(V).
Both DFS and BFS have the same time complexity (O(V + E)), but DFS typically requires less memory because it only needs to store the vertices on the current path, whereas BFS needs to store all vertices at the current level.
DFS is the backbone of backtracking algorithms, which are used to solve puzzles, generate mazes, and solve combinatorial problems like N-Queens and Sudoku.
DFS can be used to detect cycles in a graph by keeping track of vertices in the current recursion stack.
DFS can be used to perform a topological sort of a directed acyclic graph (DAG), which is useful for scheduling tasks with dependencies.
DFS can be used to find all connected components in an undirected graph and strongly connected components in a directed graph.