G-21: Topological Sort Algorithm | DFS
Topological sorting is a fundamental graph algorithm used to linearize dependent tasks, heavily utilized in build systems, course prerequisite trackers, and package managers. This chapter covers the theoretical foundations of topological sort, why it is restricted to Directed Acyclic Graphs (DAGs), and how to implement it efficiently using Depth-First Search (DFS) alongside a stack.
1. Why Do We Need Topological Sort? (Problem Statement)
In many real-world scenarios, tasks have dependencies. For instance, in a university curriculum, you cannot take Advanced Algorithms without first passing Data Structures.
- Definition: Topological sorting is a linear ordering of vertices in a directed graph such that for every directed edge from vertex to vertex (), vertex comes before vertex in the ordering.
- Key Constraint: Topological sorting only exists on Directed Acyclic Graphs (DAGs). If a graph contains cycles or undirected edges, a valid topological sort is impossible.
2. Core Mental Model & Real-Life Analogy
Real-Life Analogy: Getting Dressed
Imagine getting dressed in the morning. Certain items depend on others:
- You must put on your socks before your shoes.
- You must put on your undershirt before your jacket.
If you try to map these dependencies out, you create a directed graph where an edge points from a prerequisite to the dependent item. A topological sort gives you a valid, sequential list of instructions on what order to put your clothes on without violating any dependencies.
3. Why Only Directed Acyclic Graphs (DAGs)?
The algorithm strictly requires two graph properties: Directed and Acyclic.
-
Why Directed? If a graph is undirected, an edge between node and node implies bidirectional dependencies ( and ). This creates an impossible paradox where must appear before , and must appear before .
-
Why Acyclic (No Cycles)? If a graph contains a cycle (e.g., , , and ), it creates a circular dependency:
- must come before
- must come before
- must come before
It is mathematically impossible to arrange these in a linear sequence. Thus, cycles break topological sorting entirely.
4. Visualizing the Graph & Adjacency List
Consider a directed acyclic graph (DAG) with nodes from to and the following directed edges:
Valid Linear Orderings
Depending on how you traverse the graph, multiple valid topological orderings can exist. Two valid examples for the above graph include:
[5, 4, 2, 3, 1, 0][4, 5, 2, 3, 1, 0]
Let's verify the first ordering [5, 4, 2, 3, 1, 0] against our edge constraints:
5 appears before 0 (Valid)
4 appears before 0 (Valid)
5 appears before 2 (Valid)
2 appears before 3 (Valid)
3 appears before 1 (Valid)
4 appears before 1 (Valid)
5. Step-by-Step DFS Algorithm Walkthrough
To find the topological sort using DFS, we use a Visited Array to keep track of visited nodes and a Stack to store nodes in the order their DFS completions finish.
Algorithm Steps:
- Initialize a
visitedarray of size with all zeros (unvisited). - Initialize an empty
Stack. - Iterate through all nodes from to . If a node is not visited, trigger a DFS call on it.
- Inside the DFS function:
- Mark the current node as visited ().
- Recursively visit all unvisited adjacent neighbors.
- Once all neighbors are explored, push the current node onto the Stack before returning.
- After all nodes are processed, pop elements from the stack one by one to get the final topological ordering.
Trace Execution on Sample Graph:
- Start at Node 0: Mark visited. No unvisited neighbors. Push
0to Stack. Stack:[0] - Start at Node 1: Mark visited. No unvisited neighbors. Push
1to Stack. Stack:[0, 1] - Start at Node 2: Mark visited. Go to neighbor
3.- Node 3: Mark visited. Go to neighbor
1(already visited, skip). Push3to Stack. Stack:[0, 1, 3] - Return to
2. Push2to Stack. Stack:[0, 1, 3, 2]
- Node 3: Mark visited. Go to neighbor
- Start at Node 4: Mark visited. Neighbors
0and1are already visited. Push4to Stack. Stack:[0, 1, 3, 2, 4] - Start at Node 5: Mark visited. Neighbors
0and2are already visited. Push5to Stack. Stack:[0, 1, 3, 2, 4, 5]
Pop elements from the stack:
6. Implementation Code (C++ & Java)
C++ Implementation
class Solution {
private:
void dfs(int node, int vis[], stack<int>& st, vector<int> adj[]) {
vis[node] = 1;
for (auto it : adj[node]) {
if (!vis[it]) {
dfs(it, vis, st, adj);
}
}
st.push(node); // Push to stack after all descendants are visited
}
public:
vector<int> topologicalSort(int V, vector<int> adj[]) {
int vis[V] = {0};
stack<int> st;
for (int i = 0; i < V; i++) {
if (!vis[i]) {
dfs(i, vis, st, adj);
}
}
vector<int> ans;
while (!st.empty()) {
ans.push_back(st.top());
st.pop();
}
return ans;
}
};
Java Implementation
class Solution {
private static void dfs(int node, int vis[], Stack<int> st, ArrayList<ArrayList<Integer>> adj) {
vis[node] = 1;
for (int it : adj.get(node)) {
if (vis[it] == 0) {
dfs(it, vis, st, adj);
}
}
st.push(node);
}
public static int[] topologicalSort(ArrayList<ArrayList<Integer>> adj, int V) {
int vis[] = new int[V];
Stack<Integer> st = new Stack<>();
for (int i = 0; i < V; i++) {
if (vis[i] == 0) {
dfs(i, vis, st, adj);
}
}
int ans[] = new int[V];
int i = 0;
while (!st.isEmpty()) {
ans[i++] = st.peek();
st.pop();
}
return ans;
}
}
7. Complexity Analysis
8. Summary and Key Takeaways
- Definition Check: Topological sort provides a linear ordering of vertices in a DAG such that for every edge , precedes .
- Graph Restrictions: Valid only on Directed Acyclic Graphs (DAGs). Cycles or undirected edges make topological sorting impossible.
- The Stack Mechanism: By pushing a node to a stack only after its entire DFS branch is fully explored, we ensure that deeper dependent nodes settle at the bottom of the stack, while prerequisites settle at the top.
- Component Coverage: Always loop through all nodes from to to handle disconnected components in the graph.
- Optimal Efficiency: Achieves an optimal linear time complexity of , matching standard DFS performance.