5. Graph Traversal: Depth-First Search (DFS)
Depth-First Search (DFS) is a traversal algorithm that explores the graph by going as "deep" as possible down one path before backtracking.
5.1 DFS Analogy and Illustration
Imagine exploring a maze with only one path.
- You pick a direction (an edge) and follow it.
- You keep going, taking the first available turn at each junction (visiting the first unvisited neighbor).
- You continue until you hit a dead end (a node with no unvisited neighbors).
- At the dead end, you backtrack to the previous junction and try a different path.
DFS works this way. It is excellent for detecting cycles, topological sorting, and solving puzzles.
Example:
We want to trace BFS starting from node 0 on the following graph:
0
/ \
1---2
/ \
3 4
The traversal order will be 0, 1, 2, 4, 3.
5.2 The Role of std::stack
The LIFO (Last-In, First-Out) nature of a stack is perfect for DFS.
- Recursive DFS: The system's call stack is used implicitly. When you make a recursive call
DFSUtil(neighbor), you "push" the new function onto the stack. When you hit a dead end, the function returns, "popping" itself off the stack and automatically "backtracking" to the previous node. - Iterative DFS: We manually use a
std::stack. We push a node, then push its neighbors. The last neighbor pushed is on top, so it will be the first one we explore next, perfectly mimicking the "go deep" behavior of recursion.
5.3 DFS Implementation
5.3.1 Iterative Approach
This approach uses an explicit std::stack and avoids recursion.
void DFS(int s) {
// 1. Create a visited array, initialized to all false
std::vector<bool> visited(V, false);
// 2. Create a Stack for DFS
std::stack<int> stack;
// 3. Push the source vertex onto the stack
stack.push(s);
std::cout << "DFS Traversal (Iterative): ";
// 4. Loop as long as the stack is not empty
while (!stack.empty()) {
// 5. Pop a vertex from the top of the stack
int u = stack.top();
stack.pop();
// 6. If it hasn't been visited yet:
if (!visited[u]) {
visited[u] = true;
std::cout << u << " ";
}
// 7. Get all neighbors of 'u'
for (auto& neighbor : adj[u]) {
// 8. If the neighbor is unvisited, push it
if (!visited[neighbor]) {
stack.push(neighbor);
}
}
}
std::cout << std::endl;
}
Code Explanation:
- Initialize a
visitedarray (allfalse) and an emptystack. - Push the starting node
sonto the stack. - While the stack is not empty:
- Pop a node
ufrom the stack. - If
uhas not been visited:- Mark
uas visited and print it. - Iterate through all neighbors of
u. - If a neighbor has not been visited, push it onto the stack (to be processed next).
- Mark
- Pop a node
5.3.2 Recursive Approach
This is the most common and intuitive way to implement DFS. It uses a helper function.
// Private helper function for recursive DFS
void DFSUtil(int u, std::vector<bool>& visited) {
// 1. Mark the current node as visited and print it
visited[u] = true;
std::cout << u << " ";
// 2. Iterate over all neighbors
for (auto& neighbor : adj[u]) {
// 3. If a neighbor is unvisited:
if (!visited[neighbor]) {
// 4. Make a recursive call to "go deeper"
DFSUtil(neighbor, visited);
}
}
// 5. When all neighbors are visited, the function returns.
// This is the implicit "backtracking" step.
}
// Public wrapper function to start the DFS
void DFSRecursive(int s) {
// 1. Create a visited array
std::vector<bool> visited(V, false);
std::cout << "DFS Traversal (Recursive): ";
// 2. Call the helper function to start the recursion
DFSUtil(s, visited);
std::cout << std::endl;
}
Code Explanation:
- The
DFSRecursivefunction initializes thevisitedarray. - It calls the helper function
DFSUtilwith the starting nodes. - Inside
DFSUtil(the recursive part):- Marks the current node
uas visited and prints it. - Iterates through all neighbors of
u. - If a neighbor has not been visited, it immediately calls
DFSUtilon that neighbor, "going deeper." - When a node has no unvisited neighbors, its function call finishes and "backtracks" to the previous node's loop.
- Marks the current node
5.3.3 Comparison: Iterative vs. Recursive
Both recursive and iterative DFS accomplish the same goal, but they differ in implementation and performance characteristics.
| Aspect | Recursive DFS | Iterative DFS |
|---|---|---|
| Logic | Intuitive and clean. Backtracking is handled implicitly by function returns. | Backtracking is handled explicitly using a std::stack. |
| Data Structure | Uses the Call Stack (managed by the system). | Uses an explicit std::stack (managed by the programmer on the heap). |
| Memory Usage | Can cause a Stack Overflow error if the graph is very deep (long paths). | Safer for very deep graphs, as heap memory is much larger than stack memory. |
| Code Simplicity | Often shorter and easier to write and understand the core "go deep" logic. | Can be more complex to write, but the process is more transparent. |
In summary:
- Use Recursive DFS for simplicity and clarity, especially when the graph depth is known to be manageable.
- Use Iterative DFS when you need to avoid stack overflow on very deep graphs or when you need finer control over the traversal.
No comments to display
No comments to display