Topological Sort Using DFS in DSA Rust - Time & Space Complexity
We want to understand how the time needed to perform topological sort using DFS changes as the graph size grows.
Specifically, how the number of nodes and edges affects the work done.
Analyze the time complexity of the following code snippet.
fn dfs(node: usize, graph: &Vec<Vec<usize>>, visited: &mut Vec<bool>, stack: &mut Vec<usize>) {
visited[node] = true;
for &neighbor in &graph[node] {
if !visited[neighbor] {
dfs(neighbor, graph, visited, stack);
}
}
stack.push(node);
}
fn topological_sort(graph: &Vec<Vec<usize>>, n: usize) -> Vec<usize> {
let mut visited = vec![false; n];
let mut stack = Vec::new();
for node in 0..n {
if !visited[node] {
dfs(node, graph, &mut visited, &mut stack);
}
}
stack.reverse();
stack
}
This code performs a topological sort on a directed graph using depth-first search (DFS).
Identify the loops, recursion, array traversals that repeat.
- Primary operation: DFS visits each node and explores all its neighbors recursively.
- How many times: Each node is visited exactly once, and each edge is explored once during the DFS calls.
The work grows with the number of nodes and edges. For example:
| Input Size (n nodes, e edges) | Approx. Operations |
|---|---|
| 10 nodes, 15 edges | About 25 operations (visiting nodes + edges) |
| 100 nodes, 200 edges | About 300 operations |
| 1000 nodes, 5000 edges | About 6000 operations |
Pattern observation: The operations grow roughly proportional to nodes plus edges combined.
Time Complexity: O(n + e)
This means the time grows linearly with the total number of nodes and edges in the graph.
[X] Wrong: "DFS visits each node multiple times, so complexity is higher than linear."
[OK] Correct: Each node is marked visited and processed only once, so DFS does not repeat work on the same node.
Understanding this time complexity helps you explain how graph algorithms scale and why DFS-based topological sort is efficient for dependency resolution.
"What if the graph is represented using an adjacency matrix instead of adjacency lists? How would the time complexity change?"
Practice
Solution
Step 1: Understand DFS role in Topological Sort
DFS visits nodes deeply, ensuring all nodes reachable from a node are visited first.Step 2: Relation to dependencies
In Topological Sort, DFS ensures nodes are added after all their dependencies (incoming edges) are processed.Final Answer:
To visit nodes in a way that all dependencies come before the node -> Option CQuick Check:
DFS orders nodes respecting dependencies [OK]
- Confusing DFS with BFS for topological order
- Thinking DFS finds shortest paths
- Mixing cycle detection with topological sorting
Solution
Step 1: Check parameter passing for DFS
Graph and visited must be passed by reference to avoid copying; stack must be mutable reference to push nodes.Step 2: Analyze options
fn dfs(node: usize, graph: &Vec, visited: &mut Vec<bool>, stack: &mut Vec<usize>) { /* body */ } passes graph and visited by reference and stack as mutable reference, correct for DFS recursion.Final Answer:
fn dfs(node: usize, graph: &Vec, visited: &mut Vec<bool>, stack: &mut Vec<usize>) { /* body */ } -> Option BQuick Check:
Correct references and mutability in parameters [OK]
- Passing graph or visited by value causing copies
- Not using mutable reference for stack
- Incorrect types for graph adjacency list
fn main() {
let graph = vec![
vec![1, 2],
vec![3],
vec![3],
vec![],
];
let mut visited = vec![false; 4];
let mut stack = Vec::new();
dfs(0, &graph, &mut visited, &mut stack);
while let Some(node) = stack.pop() {
print!("{} ", node);
}
}
fn dfs(node: usize, graph: &Vec, visited: &mut Vec<bool>, stack: &mut Vec<usize>) {
if visited[node] { return; }
visited[node] = true;
for &neighbor in &graph[node] {
dfs(neighbor, graph, visited, stack);
}
stack.push(node);
}Solution
Step 1: Trace DFS calls and stack pushes
DFS starts at 0, visits 1, then 3 (push 3), back to 1 (push 1), then visits 2, then 3 (already visited), push 2, finally push 0.Step 2: Stack content and output order
Stack after DFS: [3,1,2,0]. Popping gives 0 then 2 then 1 then 3, printing "0 2 1 3".Final Answer:
0 2 1 3 -> Option AQuick Check:
DFS post-order stack reversed = topological order [OK]
- Printing stack before reversing
- Not marking visited nodes
- Confusing order of push and pop
fn dfs(node: usize, graph: &Vec, visited: &mut Vec<bool>, stack: &mut Vec<usize>) {
visited[node] = true;
for &neighbor in &graph[node] {
if !visited[neighbor] {
dfs(neighbor, graph, visited, stack);
}
}
stack.push(node);
}
fn main() {
let graph = vec![vec![1], vec![2], vec![0]];
let mut visited = vec![false; 3];
let mut stack = Vec::new();
for i in 0..3 {
if !visited[i] {
dfs(i, &graph, &mut visited, &mut stack);
}
}
while let Some(node) = stack.pop() {
print!("{} ", node);
}
}Solution
Step 1: Analyze graph edges
Edges: 0->1, 1->2, 2->0 form a cycle.Step 2: Understand topological sort limitation
Topological sort requires DAG (no cycles). Cycle presence makes ordering impossible.Final Answer:
The graph contains a cycle, so topological sort is invalid -> Option DQuick Check:
Cycle in graph prevents valid topological order [OK]
- Ignoring cycles in input graph
- Assuming topological sort works on cyclic graphs
- Not detecting cycles before sorting
Solution
Step 1: Analyze dependencies order
Task 5 after 2 and 0; 4 after 0 and 1; 2 after 3; 3 after 1.Step 2: Validate each option
1 3 0 2 4 5 respects all: 1 before 3, 3 before 2, 0 before 4 and 5, 2 and 0 before 5, 1 and 0 before 4.Final Answer:
1 3 0 2 4 5 -> Option AQuick Check:
All dependencies precede dependent tasks [OK]
- Ignoring order of dependencies
- Placing dependent tasks before prerequisites
- Confusing task numbers and dependencies
Ready to prove it?
Pass the test and this pattern moves unknown
