Topological Sort using Kahn's Algorithm
Problem and examples
Topological Sort of a directed graph is a linear ordering of its vertices such that for every directed edge u → v, vertex u comes before v in the ordering.
Kahn’s Algorithm uses a BFS-based approach and is suitable for Directed Acyclic Graphs (DAGs). It efficiently produces a valid topological ordering or detects the presence of a cycle.
| Graph (Edges) | Topological Order | Description |
|---|---|---|
| [[5, 2], [5, 0], [4, 0], [4, 1], [2, 3], [3, 1]] | [4, 5, 2, 3, 1, 0] | Valid topological sort respecting all dependencies |
| [[0, 1], [1, 2], [2, 3]] | [0, 1, 2, 3] | Simple linear DAG |
| [] | [] | Empty graph has no vertices to sort |
| [[0, 1], [1, 0]] | null | Graph contains a cycle, so topological sort is not possible |
| [[1, 3], [2, 3], [3, 4]] | [1, 2, 3, 4] | Multiple valid orderings possible |
How it works
Understanding the Problem
Topological Sort is used to linearly order the vertices of a Directed Acyclic Graph (DAG) such that for every directed edge u → v, vertex u comes before v in the order. This is often used in scheduling problems, like task execution order based on dependencies.
We will solve this using Kahn’s Algorithm, which uses a Breadth-First Search (BFS) approach based on indegree counts.
Step-by-Step Solution with Example
Step 1: Represent the Graph
We represent the graph using an adjacency list and also track the indegree (number of incoming edges) for each node.
Example: Given edges: [[5, 0], [4, 0], [5, 2], [2, 3], [3, 1], [4, 1]]
This means:
- 5 → 0
- 4 → 0
- 5 → 2
- 2 → 3
- 3 → 1
- 4 → 1
Step 2: Compute Indegrees
We loop over all edges and count how many edges point to each vertex.
After processing the above edges, indegrees would look like:
0: 2
1: 2
2: 1
3: 1
4: 0
5: 0
Step 3: Initialize Queue
We add all vertices with indegree 0 to a queue. These are the starting points because they have no dependencies.
Queue: [4, 5]
Step 4: BFS Traversal
While the queue is not empty:
- Dequeue a node and add it to the result.
- For each of its neighbors, reduce their indegree by 1.
- If a neighbor’s indegree becomes 0, add it to the queue.
Progress:
Queue: [4, 5] → [5, 0] → [0, 2] → [2, 3] → [3, 1] → [1]
Result: [4, 5, 0, 2, 3, 1]
Step 5: Check for Cycles
If at the end, the result does not contain all nodes, it means a cycle exists and topological sort is not possible. If the size of the result equals the number of vertices, we have a valid topological order.
Edge Cases
Case 1: Empty Graph
If the graph has no vertices, return an empty list. No work to be done.
Case 2: Single Node with No Edges
The output is simply the node itself.
Case 3: Multiple Starting Nodes
When multiple nodes have indegree 0, the result may vary depending on which is dequeued first. All are valid topological orders.
Case 4: Cycle in Graph
If there's a cycle, like 1 → 2 → 3 → 1, no topological sort is possible. Our result will have fewer nodes than expected, indicating a cycle.
Finally
Kahn’s Algorithm is a simple yet powerful way to solve topological sorting using a queue and indegree tracking. It not only provides the topological order but also helps in detecting cycles in the graph. This is especially useful in real-life applications like course prerequisites, task scheduling, and build systems.
- Compute the indegree of each vertex.
- Initialize a queue and enqueue all vertices with indegree 0.
- Initialize an empty list to store the topological order.
- While the queue is not empty:
- Dequeue a vertex
uand add it to the result list. - For each neighbor
vofu: - Decrease
indegree[v]by 1. - If
indegree[v] == 0, enqueuev. - If the result contains all vertices, return the result.
- Else, return
null(cycle detected).
Implementation and efficiency
C implementation
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
void topologicalSortKahn(int V, int edges[][2], int E) {
int indegree[MAX] = {0};
int adj[MAX][MAX] = {0};
int queue[MAX], front = 0, rear = 0;
int result[MAX], idx = 0;
for (int i = 0; i < E; i++) {
int u = edges[i][0];
int v = edges[i][1];
adj[u][v] = 1;
indegree[v]++;
}
for (int i = 0; i < V; i++) {
if (indegree[i] == 0)
queue[rear++] = i;
}
while (front < rear) {
int node = queue[front++];
result[idx++] = node;
for (int i = 0; i < V; i++) {
if (adj[node][i]) {
indegree[i]--;
if (indegree[i] == 0)
queue[rear++] = i;
}
}
}
if (idx != V) {
printf("Cycle detected\n");
return;
}
printf("Topological Sort: ");
for (int i = 0; i < idx; i++)
printf("%d ", result[i]);
printf("\n");
}
int main() {
int V = 6;
int edges[][2] = {{5, 2}, {5, 0}, {4, 0}, {4, 1}, {2, 3}, {3, 1}};
int E = sizeof(edges)/sizeof(edges[0]);
topologicalSortKahn(V, edges, E);
return 0;
}C++ implementation
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
void topologicalSortKahn(int V, vector<pair<int, int>> &edges) {
vector<int> indegree(V, 0);
vector<vector<int>> graph(V);
for (auto &[u, v] : edges) {
graph[u].push_back(v);
indegree[v]++;
}
queue<int> q;
for (int i = 0; i < V; ++i)
if (indegree[i] == 0) q.push(i);
vector<int> result;
while (!q.empty()) {
int node = q.front(); q.pop();
result.push_back(node);
for (int neighbor : graph[node]) {
if (--indegree[neighbor] == 0)
q.push(neighbor);
}
}
if (result.size() != V)
cout << "Cycle detected" << endl;
else {
cout << "Topological Sort: ";
for (int node : result) cout << node << " ";
cout << endl;
}
}
int main() {
int V = 6;
vector<pair<int, int>> edges = {{5, 2}, {5, 0}, {4, 0}, {4, 1}, {2, 3}, {3, 1}};
topologicalSortKahn(V, edges);
return 0;
}Python implementation
from collections import deque, defaultdict
def topological_sort_kahn(V, edges):
indegree = [0] * V
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
indegree[v] += 1
queue = deque([i for i in range(V) if indegree[i] == 0])
result = []
while queue:
node = queue.popleft()
result.append(node)
for neighbor in graph[node]:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
if len(result) == V:
print("Topological Sort:", result)
else:
print("Cycle detected")
V = 6
edges = [(5, 2), (5, 0), (4, 0), (4, 1), (2, 3), (3, 1)]
topological_sort_kahn(V, edges)Java implementation
import java.util.*;
public class TopologicalSortKahn {
public static void main(String[] args) {
int V = 6;
int[][] edges = {{5,2}, {5,0}, {4,0}, {4,1}, {2,3}, {3,1}};
topologicalSortKahn(V, edges);
}
public static void topologicalSortKahn(int V, int[][] edges) {
int[] indegree = new int[V];
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < V; i++) graph.add(new ArrayList<>());
for (int[] edge : edges) {
graph.get(edge[0]).add(edge[1]);
indegree[edge[1]]++;
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < V; i++) if (indegree[i] == 0) queue.add(i);
List<Integer> result = new ArrayList<>();
while (!queue.isEmpty()) {
int node = queue.poll();
result.add(node);
for (int neighbor : graph.get(node)) {
if (--indegree[neighbor] == 0)
queue.add(neighbor);
}
}
if (result.size() != V)
System.out.println("Cycle detected");
else
System.out.println("Topological Sort: " + result);
}
}JavaScript implementation
function topologicalSortKahn(V, edges) {
const indegree = Array(V).fill(0);
const graph = Array.from({ length: V }, () => []);
const result = [];
for (const [u, v] of edges) {
graph[u].push(v);
indegree[v]++;
}
const queue = [];
for (let i = 0; i < V; i++) {
if (indegree[i] === 0) queue.push(i);
}
while (queue.length) {
const node = queue.shift();
result.push(node);
for (const neighbor of graph[node]) {
indegree[neighbor]--;
if (indegree[neighbor] === 0) queue.push(neighbor);
}
}
console.log(result.length === V ? "Topological Sort: " + result : "Cycle detected");
}
const V = 6;
const edges = [[5, 2], [5, 0], [4, 0], [4, 1], [2, 3], [3, 1]];
topologicalSortKahn(V, edges);Go implementation
package main
import (
"fmt"
)
func topologicalSortKahn(V int, edges [][2]int) {
indegree := make([]int, V)
graph := make([][]int, V)
for _, e := range edges {
u, v := e[0], e[1]
graph[u] = append(graph[u], v)
indegree[v]++
}
queue := []int{}
for i := 0; i < V; i++ {
if indegree[i] == 0 {
queue = append(queue, i)
}
}
result := []int{}
for len(queue) > 0 {
node := queue[0]
queue = queue[1:]
result = append(result, node)
for _, neighbor := range graph[node] {
indegree[neighbor]--
if indegree[neighbor] == 0 {
queue = append(queue, neighbor)
}
}
}
if len(result) != V {
fmt.Println("Cycle detected")
} else {
fmt.Println("Topological Sort:", result)
}
}
func main() {
V := 6
edges := [][2]int{{5, 2}, {5, 0}, {4, 0}, {4, 1}, {2, 3}, {3, 1}}
topologicalSortKahn(V, edges)
}Rust implementation
use std::collections::VecDeque;
fn topological_sort_kahn(v: usize, edges: &[(usize, usize)]) {
let mut indegree = vec![0; v];
let mut graph = vec![vec![]; v];
for &(u, w) in edges {
graph[u].push(w);
indegree[w] += 1;
}
let mut queue: VecDeque<usize> = (0..v).filter(|&i| indegree[i] == 0).collect();
let mut result = vec![];
while let Some(node) = queue.pop_front() {
result.push(node);
for &neighbor in &graph[node] {
indegree[neighbor] -= 1;
if indegree[neighbor] == 0 {
queue.push_back(neighbor);
}
}
}
if result.len() == v {
println!("Topological Sort: {:?}", result);
} else {
println!("Cycle detected");
}
}
fn main() {
let v = 6;
let edges = [(5, 2), (5, 0), (4, 0), (4, 1), (2, 3), (3, 1)];
topological_sort_kahn(v, &edges);
}Kotlin implementation
fun topologicalSortKahn(V: Int, edges: List<Pair<Int, Int>>) {
val indegree = IntArray(V)
val graph = List(V) { mutableListOf<Int>() }
for ((u, v) in edges) {
graph[u].add(v)
indegree[v]++
}
val queue = ArrayDeque<Int>()
for (i in 0 until V) if (indegree[i] == 0) queue.add(i)
val result = mutableListOf<Int>()
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
result.add(node)
for (neighbor in graph[node]) {
indegree[neighbor]--
if (indegree[neighbor] == 0) queue.add(neighbor)
}
}
if (result.size != V)
println("Cycle detected")
else
println("Topological Sort: $result")
}
fun main() {
val V = 6
val edges = listOf(5 to 2, 5 to 0, 4 to 0, 4 to 1, 2 to 3, 3 to 1)
topologicalSortKahn(V, edges)
}Swift implementation
import Foundation
func topologicalSortKahn(_ V: Int, _ edges: [(Int, Int)]) {
var indegree = [Int](repeating: 0, count: V)
var graph = [[Int]](repeating: [], count: V)
for (u, v) in edges {
graph[u].append(v)
indegree[v] += 1
}
var queue = [Int]()
for i in 0..<V where indegree[i] == 0 {
queue.append(i)
}
var result = [Int]()
while !queue.isEmpty {
let node = queue.removeFirst()
result.append(node)
for neighbor in graph[node] {
indegree[neighbor] -= 1
if indegree[neighbor] == 0 {
queue.append(neighbor)
}
}
}
if result.count != V {
print("Cycle detected")
} else {
print("Topological Sort:", result)
}
}
let V = 6
let edges = [(5,2), (5,0), (4,0), (4,1), (2,3), (3,1)]
topologicalSortKahn(V, edges)TypeScript implementation
function topologicalSortKahn(V: number, edges: [number, number][]): number[] | null {
const indegree = Array(V).fill(0);
const graph: number[][] = Array.from({ length: V }, () => []);
const result: number[] = [];
for (const [u, v] of edges) {
graph[u].push(v);
indegree[v]++;
}
const queue: number[] = [];
for (let i = 0; i < V; i++) {
if (indegree[i] === 0) queue.push(i);
}
while (queue.length) {
const node = queue.shift()!;
result.push(node);
for (const neighbor of graph[node]) {
indegree[neighbor]--;
if (indegree[neighbor] === 0) queue.push(neighbor);
}
}
return result.length === V ? result : null;
}
const V = 6;
const edges: [number, number][] = [[5, 2], [5, 0], [4, 0], [4, 1], [2, 3], [3, 1]];
const result = topologicalSortKahn(V, edges);
console.log(result ? "Topological Sort: " + result : "Cycle detected");Time complexity
| Case | Time | Explanation |
|---|---|---|
| best case | O(V + E) | Even in the best case, we must visit all vertices and all edges once to compute indegrees and process the graph structure. |
| average case | O(V + E) | For any valid DAG, each vertex and edge is processed exactly once. |
| worst case | O(V + E) | Regardless of graph structure (as long as it is a DAG), we always have to check every node and every edge at least once. |
Space complexity
O(V + E)
We use additional space for the adjacency list (O(E)), indegree array (O(V)), the queue (O(V)), and result list (O(V)).