Topological Sort
Introduction
Topological Sort is a linear ordering of vertices in a directed graph such that for every directed edge (u, v), vertex u comes before vertex v in the ordering. In simpler terms, if there's a path from node A to node B in the graph, then node A appears before node B in the topological ordering.
This algorithm is particularly useful when you need to schedule tasks with dependencies. For instance, if task B depends on task A being completed first, we could represent this as a directed edge from A to B. Topological Sort would then give us a valid sequence to complete all tasks without violating any dependencies.
Topological Sort only works on Directed Acyclic Graphs (DAGs). If the graph contains a cycle, then a valid topological ordering isn't possible.
Understanding Topological Sort
Prerequisites
Before diving into Topological Sort, make sure you understand:
- Basic graph concepts
- Directed graphs
- Graph traversal algorithms (DFS/BFS)
Visual Representation
Let's understand Topological Sort visually:
In this graph:
- A directed edge from A to B means B depends on A
- One valid topological ordering would be: A, B, C, D
- Another valid ordering could be: A, C, B, D
Both orderings ensure that a vertex appears before all vertices it has edges to.
Implementations
There are two common ways to implement Topological Sort:
- Kahn's Algorithm (using BFS)
- DFS-based approach
Let's explore both:
DFS-based Topological Sort
function topologicalSort(graph) {
const visited = new Set();
const stack = [];
function dfs(node) {
visited.add(node);
// Visit all neighbors
for (const neighbor of graph[node]) {
if (!visited.has(neighbor)) {
dfs(neighbor);
}
}
// Add current node to the beginning of result
stack.unshift(node);
}
// Run DFS on all unvisited vertices
for (const node in graph) {
if (!visited.has(node)) {
dfs(node);
}
}
return stack;
}
// Example usage
const graph = {
'A': ['B', 'C'],
'B': ['D'],
'C': ['D'],
'D': []
};
console.log(topologicalSort(graph)); // Output: ['A', 'C', 'B', 'D']
Kahn's Algorithm (BFS-based Topological Sort)
function kahnTopologicalSort(graph) {
// Calculate in-degree for each vertex
const inDegree = {};
const result = [];
const queue = [];
// Initialize in-degree for all nodes to 0
for (const node in graph) {
inDegree[node] = 0;
}
// Calculate in-degree for each node
for (const node in graph) {
for (const neighbor of graph[node]) {
inDegree[neighbor] = (inDegree[neighbor] || 0) + 1;
}
}
// Add all nodes with in-degree 0 to the queue
for (const node in inDegree) {
if (inDegree[node] === 0) {
queue.push(node);
}
}
// Process nodes in queue
while (queue.length > 0) {
const current = queue.shift();
result.push(current);
// Reduce in-degree of neighbors
for (const neighbor of graph[current]) {
inDegree[neighbor]--;
// If in-degree becomes 0, add to queue
if (inDegree[neighbor] === 0) {
queue.push(neighbor);
}
}
}
// Check if we've visited all nodes
if (result.length !== Object.keys(graph).length) {
return "Graph has a cycle, topological sort not possible!";
}
return result;
}
// Example usage
const graph = {
'A': ['B', 'C'],
'B': ['D'],
'C': ['D'],
'D': []
};
console.log(kahnTopologicalSort(graph)); // Output: ['A', 'B', 'C', 'D'] or ['A', 'C', 'B', 'D']
Time and Space Complexity
For both algorithms:
- Time Complexity: O(V + E), where V is the number of vertices and E is the number of edges
- Space Complexity: O(V)
Detecting Cycles in a Graph
An important aspect of Topological Sort is detecting cycles. If a graph has a cycle, a valid topological ordering isn't possible. Both algorithms can be modified to detect cycles:
- In the DFS approach: Use a separate set to track nodes in the current recursion stack.
- In Kahn's algorithm: If the result array doesn't contain all vertices, there's a cycle.
Real-World Applications
Topological Sort has many practical applications:
1. Task Scheduling
Consider a software build system where certain libraries must be compiled before others:
A valid build order using topological sort would be: Core → Data → UI → Reports
2. Course Prerequisites
In college, some courses require prerequisites:
const courseGraph = {
'Calculus I': ['Calculus II'],
'Calculus II': ['Differential Equations'],
'Programming Basics': ['Data Structures', 'Algorithms'],
'Data Structures': ['Algorithms'],
'Algorithms': ['Advanced Algorithms'],
'Differential Equations': []
};
console.log(topologicalSort(courseGraph));
// Possible output: ['Calculus I', 'Programming Basics', 'Calculus II', 'Data Structures', 'Differential Equations', 'Algorithms', 'Advanced Algorithms']
3. Package Dependencies
In package managers like npm or pip, packages have dependencies that must be installed first:
Topological sort ensures dependencies are installed in the correct order.
Common Pitfalls and Tips
- Remember to check for cycles: A DAG shouldn't have cycles.
- Multiple valid orderings: There can be multiple valid topological sorts for a graph.
- Empty graph: A graph with no edges has a trivial topological ordering.
- Disconnected components: Process each component separately.
Practice Exercise: Course Scheduler
Problem: Given a list of courses and their prerequisites, determine if you can finish all courses.
Input:
- Number of courses
n
(labeled from 0 to n-1) - Prerequisites as pairs
[a, b]
indicating coursea
requires courseb
Example:
numCourses = 4;
prerequisites = [[1,0], [2,0], [3,1], [3,2]];
// Can be visualized as:
// 0 -> 1 -> 3
// | ^
// └-> 2 ----┘
Solution using Topological Sort:
function canFinishCourses(numCourses, prerequisites) {
// Create adjacency list
const graph = {};
for (let i = 0; i < numCourses; i++) {
graph[i] = [];
}
// Build the graph
for (const [course, prereq] of prerequisites) {
graph[prereq].push(course);
}
// Track visited nodes and nodes in current path
const visited = new Set();
const path = new Set();
function hasCycle(node) {
// If node is in current path, we found a cycle
if (path.has(node)) return true;
// If already processed and no cycle found
if (visited.has(node)) return false;
path.add(node);
// Check all neighbors for cycles
for (const neighbor of graph[node]) {
if (hasCycle(neighbor)) return true;
}
path.delete(node);
visited.add(node);
return false;
}
// Check each node for cycles
for (let i = 0; i < numCourses; i++) {
if (hasCycle(i)) return false;
}
return true;
}
// Test
console.log(canFinishCourses(4, [[1,0], [2,0], [3,1], [3,2]])); // true
console.log(canFinishCourses(2, [[1,0], [0,1]])); // false (cycle)
Summary
Topological Sort is a powerful algorithm for processing directed acyclic graphs. It orders vertices such that all directed edges go from earlier to later vertices in the sequence.
Key takeaways:
- Only works on directed acyclic graphs (DAGs)
- Can be implemented using either DFS or BFS (Kahn's algorithm)
- Has time complexity O(V + E)
- Useful for dependency resolution, task scheduling, and more
- Multiple valid topological orderings may exist
Additional Resources
- Try implementing both algorithms from scratch
- Solve these classic problems that use topological sort:
- Course Schedule (LeetCode #207)
- Alien Dictionary (LeetCode #269)
- Minimum Height Trees (LeetCode #310)
Practice Exercises
- Implement a function to detect if a graph has a valid topological ordering
- Find all possible topological orderings of a given graph
- Solve the "Build Order" problem: Given a list of projects and dependencies, find a build order that allows building all projects
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)