Implementing Topological Sort
1. Understanding Topological Ordering
Topological sorting is a linear ordering of vertices in a directed acyclic graph (DAG) such that for every directed edge u → v, vertex u comes before v in the ordering.
| Property | Description | Notes |
|---|---|---|
| Directed Acyclic Graph (DAG) | A graph with directed edges and no cycles | Topological sort is only possible on DAGs |
| In-degree | Number of incoming edges to a vertex | Used in Kahn's algorithm for topological sort |
| Linear Ordering | Each vertex appears once in the sequence | Not unique; multiple valid orderings may exist |
Example: Basic Concept
const graph = {
'A': ['B'],
'B': ['C'],
'C': []
};
// Valid topological order: A → B → C
Steps to Perform Topological Sort
- Calculate in-degrees for all vertices
- Add all vertices with in-degree 0 to a queue
- Process the queue, reducing in-degrees of neighbors
- If any vertex has in-degree > 0 after processing, a cycle exists
2. Implementing Kahn's Algorithm
Kahn's algorithm is a classic approach for performing topological sorting on directed acyclic graphs (DAGs). It works by repeatedly removing nodes with no incoming edges and updating the graph accordingly.
| Step | Description | Action |
|---|---|---|
| 1 | Calculate in-degrees for all nodes | Count incoming edges for each node |
| 2 | Initialize queue with nodes having in-degree 0 | Add all nodes with zero in-degrees to a queue |
| 3 | Process nodes in queue | Remove node from queue, add to result, reduce in-degrees of neighbors |
| 4 | Update in-degrees | Decrease in-degree of neighbors when a node is processed |
| 5 | Repeat until queue is empty | Continue processing nodes with in-degree 0 |
| 6 | Check for cycles | If result size is less than total nodes, graph has a cycle |
Example: Kahn's Algorithm Implementation in JavaScript
function topologicalSortKahn(vertices, edges) {
const inDegree = new Array(vertices).fill(0);
const adjList = Array(vertices)
.fill()
.map(() => []);
// Build adjacency list and in-degree array
for (const [from, to] of edges) {
adjList[from].push(to);
inDegree[to]++;
}
const queue = [];
for (let i = 0; i < vertices; i++) {
if (inDegree[i] === 0) queue.push(i);
}
const result = [];
while (queue.length > 0) {
const node = queue.shift();
result.push(node);
for (const neighbor of adjList[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) queue.push(neighbor);
}
}
return result.length === vertices ? result : [];
}
// Example usage:
const vertices = 6;
const edges = [
[5, 2],
[5, 0],
[4, 0],
[4, 1],
[2, 3],
[3, 1]
];
console.log(topologicalSortKahn(vertices, edges)); // [5, 4, 2, 3, 1, 0]
Algorithm Summary
- Time Complexity: O(V + E), where V is the number of vertices and E is the number of edges
- Space Complexity: O(V + E) for storing the adjacency list and in-degree array
- Use Case: Detecting cycles, scheduling tasks with dependencies
3. Implementing DFS-based Topological Sort
Topological sorting using Depth-First Search (DFS) is a classic algorithm for ordering vertices in a Directed Acyclic Graph (DAG). It works by performing DFS and pushing nodes to a stack once all their descendants are visited.
| Step | Description | Implementation |
|---|---|---|
| 1. Initialize | Mark all nodes as unvisited and create an empty stack. | const visited = new Set();\nconst stack = []; |
| 2. DFS Traversal | For each unvisited node, perform DFS. | function dfs(node) {\n visited.add(node);\n for (const neighbor of graph[node]) {\n if (!visited.has(neighbor)) {\n dfs(neighbor);\n }\n }\n stack.push(node);\n} |
| 3. Build Result | Pop elements from the stack to get topological order. | const result = [];\nwhile (stack.length > 0) {\n result.push(stack.pop());\n}\nreturn result; |
Example: DFS-based Topological Sort
const graph = {\n 'A': ['B'],\n 'B': ['C'],\n 'C': []\n};\n\nfunction topologicalSort(graph) {\n const visited = new Set();\n const stack = [];\n\n function dfs(node) {\n visited.add(node);\n for (const neighbor of graph[node]) {\n if (!visited.has(neighbor)) {\n dfs(neighbor);\n }\n }\n stack.push(node);\n }\n\n for (const node in graph) {\n if (!visited.has(node)) {\n dfs(node);\n }\n }\n\n const result = [];\n while (stack.length > 0) {\n result.push(stack.pop());\n }\n return result;\n}\n\nconsole.log(topologicalSort(graph)); // ['A', 'B', 'C']
4. Detecting Cycle in Directed Graph
| Method | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| DFS-based Detection | Uses DFS to detect back edges in a directed graph. If a back edge exists, a cycle is present. | O(V + E) |
O(V) |
| Kahn's Algorithm (BFS) | Uses in-degree tracking and BFS to detect cycles by ensuring all nodes are processed. | O(V + E) |
O(V) |
Example: DFS-based Cycle Detection
function hasCycleDFS(graph) {
const visited = new Set();
const recStack = new Set();
for (let node in graph) {
if (!visited.has(node)) {
if (detectCycleUtil(graph, node, visited, recStack)) {
return true;
}
}
}
return false;
}
function detectCycleUtil(graph, node, visited, recStack) {
visited.add(node);
recStack.add(node);
for (let neighbor of graph[node]) {
if (!visited.has(neighbor)) {
if (detectCycleUtil(graph, neighbor, visited, recStack)) {
return true;
}
} else if (recStack.has(neighbor)) {
return true;
}
}
recStack.delete(node);
return false;
}
Example: Kahn's Algorithm for Cycle Detection
function hasCycleKahn(graph) {
const inDegree = {};
for (let node in graph) {
inDegree[node] = 0;
}
for (let node in graph) {
for (let neighbor of graph[node]) {
inDegree[neighbor]++;
}
}
const queue = [];
for (let node in inDegree) {
if (inDegree[node] === 0) {
queue.push(node);
}
}
let processedCount = 0;
while (queue.length > 0) {
const node = queue.shift();
processedCount++;
for (let neighbor of graph[node]) {
inDegree[neighbor]--;
if (inDegree[neighbor] === 0) {
queue.push(neighbor);
}
}
}
return processedCount !== Object.keys(graph).length;
}
5. Finding All Topological Orderings
Topological sorting is not unique for a given directed acyclic graph (DAG). There can be multiple valid orderings of nodes that respect the dependencies. Finding all topological orderings involves exploring all possible valid sequences.
| Property | Description | Notes |
|---|---|---|
| Algorithm | Backtracking-based approach to generate all valid orderings | Starts with nodes having in-degree 0, recursively explores all valid next nodes |
| Time Complexity | O(V! × E) | Where V is the number of vertices and E is the number of edges. In worst case, all permutations are valid |
| Space Complexity | O(V) | For storing the in-degree array and recursion stack |
Example: Finding All Orderings
function findAllTopologicalOrderings(graph) {
const vertices = Object.keys(graph);
const inDegree = {};
const result = [];
// Initialize in-degrees
vertices.forEach(v => inDegree[v] = 0);
vertices.forEach(v => {
graph[v].forEach(n => inDegree[n]++);
});
const backtrack = (currentOrder) => {
if (currentOrder.length === vertices.length) {
result.push([...currentOrder]);
return;
}
for (const v of vertices) {
if (inDegree[v] === 0) {
// Choose
currentOrder.push(v);
const tempInDegree = { ...inDegree };
graph[v].forEach(n => inDegree[n]--);
// Explore
backtrack(currentOrder);
// Unchoose (backtrack)
currentOrder.pop();
Object.assign(inDegree, tempInDegree);
}
}
};
backtrack([]);
return result;
}
// Example usage:
const graph = {
'A': ['C'],
'B': ['C'],
'C': ['D'],
'D': []
};
console.log(findAllTopologicalOrderings(graph));
// Output: [['A', 'B', 'C', 'D'], ['B', 'A', 'C', 'D']]
6. Scheduling Tasks with Prerequisites
Topological sort is commonly used to schedule tasks that have prerequisites. It ensures that a task is only scheduled after all its dependencies are completed.
| Task | Prerequisites | Order |
|---|---|---|
| Task A | [] |
1 |
| Task B | ['A'] |
2 |
| Task C | ['A', 'B'] |
3 |
Example: Scheduling Tasks Using Topological Sort
const tasks = ['A', 'B', 'C'];
const prerequisites = [
['A', 'B'],
['B', 'C']
];
function scheduleTasks(tasks, prerequisites) {
const graph = new Map();
const inDegree = new Map();
// Initialize graph and in-degree
tasks.forEach(task => {
graph.set(task, []);
inDegree.set(task, 0);
});
// Build the graph
prerequisites.forEach(([prereq, task]) => {
graph.get(prereq).push(task);
inDegree.set(task, inDegree.get(task) + 1);
});
// Find tasks with no prerequisites
const queue = [];
inDegree.forEach((degree, task) => {
if (degree === 0) queue.push(task);
});
const result = [];
while (queue.length > 0) {
const task = queue.shift();
result.push(task);
// Reduce in-degree of dependent tasks
graph.get(task).forEach(dependent => {
inDegree.set(dependent, inDegree.get(dependent) - 1);
if (inDegree.get(dependent) === 0) {
queue.push(dependent);
}
});
}
return result;
}
console.log(scheduleTasks(tasks, prerequisites)); // ['A', 'B', 'C']
7. Finding Minimum Height Trees
Minimum Height Trees (MHTs) are trees with the smallest possible height when rooted at any node. The height of a tree is defined as the maximum distance from the root to any leaf node.
| Property | Description | Notes |
|---|---|---|
| Tree Representation | Graph with n nodes and n-1 edges, connected and acyclic | Each node has a degree (number of neighbors) |
| Height Definition | Maximum depth of any node from root | Minimized by choosing optimal root(s) |
| Root Selection | Optimal root(s) are the center(s) of the tree | Center is the middle node(s) of the longest path |
Example: Finding MHT Roots
function findMinHeightTrees(n, edges) {
if (n === 1) return [0];
const adj = Array(n).fill().map(() => []);
const degree = Array(n).fill(0);
for (const [u, v] of edges) {
adj[u].push(v);
adj[v].push(u);
degree[u]++;
degree[v]++;
}
const leaves = [];
for (let i = 0; i < n; i++) {
if (degree[i] === 1) leaves.push(i);
}
let remaining = n;
while (remaining > 2) {
const newLeaves = [];
for (const leaf of leaves) {
const neighbor = adj[leaf].pop();
degree[neighbor]--;
if (degree[neighbor] === 1) newLeaves.push(neighbor);
}
leaves.splice(0, leaves.length, ...newLeaves);
remaining -= leaves.length;
}
return leaves;
}
// Example usage:
const n = 4;
const edges = [[1, 0], [1, 2], [1, 3]];
console.log(findMinHeightTrees(n, edges)); // Output: [1]
Algorithm Summary
- Build adjacency list and compute degrees of each node
- Start with all leaf nodes (degree 1) in a queue
- Repeatedly remove leaves and update neighbor degrees
- Continue until 1 or 2 nodes remain — these are the MHT roots
8. Finding Alien Dictionary Order
Given a list of words sorted lexicographically according to an alien language's alphabet order, reconstruct the possible alphabetical order of the language.
| Property | Description | Notes |
|---|---|---|
| Input | List of words in lexicographically sorted order | Words are from the alien dictionary |
| Output | Valid topological order of the alien alphabet | May have multiple valid orders |
| Approach | Build a graph from adjacent word comparisons and perform topological sort | Each pair of words gives a constraint on character order |
Example: Alien Dictionary Order
const words = ["wrt", "wrf", "er", "ett", "rtff"];
function alienOrder(words) {
const graph = new Map();
const inDegree = new Map();
// Initialize all characters
for (let word of words) {
for (let char of word) {
if (!graph.has(char)) {
graph.set(char, []);
inDegree.set(char, 0);
}
}
}
// Build graph from adjacent words
for (let i = 0; i < words.length - 1; i++) {
const word1 = words[i];
const word2 = words[i + 1];
let j = 0;
while (j < word1.length && j < word2.length) {
const char1 = word1[j];
const char2 = word2[j];
if (char1 !== char2) {
// Add edge from char1 to char2
if (!graph.get(char1).includes(char2)) {
graph.get(char1).push(char2);
inDegree.set(char2, inDegree.get(char2) + 1);
}
break;
}
j++;
}
// Check for invalid case: longer word comes before shorter one with same prefix
if (j < word1.length && j === word2.length) {
return "";
}
}
// Topological sort using Kahn's algorithm
const queue = [];
for (let [char, degree] of inDegree) {
if (degree === 0) queue.push(char);
}
const result = [];
while (queue.length > 0) {
const char = queue.shift();
result.push(char);
for (let neighbor of graph.get(char)) {
inDegree.set(neighbor, inDegree.get(neighbor) - 1);
if (inDegree.get(neighbor) === 0) {
queue.push(neighbor);
}
}
}
// Check if all characters were included (no cycles)
return result.length === inDegree.size ? result.join("") : "";
}
console.log(alienOrder(words)); // "wertf"
9. Reconstructing Sequence
Topological sorting can be used to reconstruct a sequence of tasks or items based on their dependencies. This is particularly useful in project management, build systems, and scheduling.
| Step | Description | Action |
|---|---|---|
| 1 | Identify all nodes with in-degree 0 | Start with tasks that have no prerequisites |
| 2 | Process nodes in order | Add each node to the result sequence and reduce in-degrees of dependent nodes |
| 3 | Repeat until all nodes are processed | Continue until no more nodes with in-degree 0 exist |
Example: Reconstructing a Build Order
const tasks = [
{ id: 'A', dependencies: [] },
{ id: 'B', dependencies: ['A'] },
{ id: 'C', dependencies: ['A'] },
{ id: 'D', dependencies: ['B', 'C'] }
];
function reconstructSequence(tasks) {
const inDegree = new Map();
const graph = new Map();
// Initialize in-degree and graph
tasks.forEach(task => {
inDegree.set(task.id, 0);
graph.set(task.id, []);
});
tasks.forEach(task => {
task.dependencies.forEach(dep => {
if (graph.has(dep)) {
graph.get(dep).push(task.id);
inDegree.set(task.id, inDegree.get(task.id) + 1);
}
});
});
const queue = [];
const result = [];
// Find all nodes with in-degree 0
inDegree.forEach((degree, node) => {
if (degree === 0) queue.push(node);
});
// Process nodes
while (queue.length > 0) {
const current = queue.shift();
result.push(current);
const neighbors = graph.get(current) || [];
neighbors.forEach(node => {
inDegree.set(node, inDegree.get(node) - 1);
if (inDegree.get(node) === 0) {
queue.push(node);
}
});
}
return result;
}
console.log(reconstructSequence(tasks)); // ['A', 'B', 'C', 'D']
Key Points
- Topological sort is used to determine a valid order of tasks with dependencies
- Reconstructing a sequence involves processing nodes with zero in-degree
- Cycles in dependencies prevent a valid sequence from being constructed
10. Finding Valid Build Order
| Property | Description | Usage |
|---|---|---|
| buildOrder | Array of tasks in valid order to execute | topologicalSort(tasks, dependencies) |
| dependencies | Map of task to its prerequisites | { task: [prereq1, prereq2] } |
Example: Finding Valid Build Order
function topologicalSort(tasks, dependencies) {
const inDegree = {};
tasks.forEach(task => inDegree[task] = 0);
Object.keys(dependencies).forEach(task => {
dependencies[task].forEach(prereq => {
inDegree[prereq] = (inDegree[prereq] || 0) + 1;
});
});
const queue = [];
const result = [];
Object.keys(inDegree).forEach(task => {
if (inDegree[task] === 0) queue.push(task);
});
while (queue.length > 0) {
const task = queue.shift();
result.push(task);
if (dependencies[task]) {
dependencies[task].forEach(prereq => {
inDegree[prereq]--;
if (inDegree[prereq] === 0) queue.push(prereq);
});
}
}
return result.length === tasks.length ? result : [];
}
const tasks = ['A', 'B', 'C', 'D'];
const dependencies = {
'B': ['A'],
'C': ['A'],
'D': ['B', 'C']
};
console.log(topologicalSort(tasks, dependencies)); // ['A', 'B', 'C', 'D']