THN Interview Prep

12. Topological Sort

TL;DR

Topological sort produces a linear ordering of vertices in a directed acyclic graph (DAG) such that every edge u → v has u before v. Kahn’s algorithm repeatedly peels vertices with in-degree zero (BFS on the “ready” layer). DFS with three colors finishes with a reverse post-order list and detects cycles (back edges to gray vertices).

Recognition Cues

  • Phrases: “prerequisites,” “course schedule,” “ordering with constraints,” “dependencies,” “build order,” “Compilation order,” “Alien dictionary characters.”
  • Input: directed edges, often numCourses + edge list.
  • Output: any valid order, or impossible if cycle; sometimes lexicographically smallest (tie-break via min-heap).

Diagram

Pattern control flow (customize nodes for this pattern). camelCase node IDs.

Loading diagram…

Mental Model

In-degree counts “how many parents must happen first.” When it hits zero, the task is ready. If you ever exhaust ready nodes but unfinished work remains, there is a cycle. In DFS coloring: white = unseen, gray = on recursion stack, black = done; an edge to gray means cycle. Topological order is reverse of DFS finish times.

Generic Recipe

Kahn

  1. Build adjacency list and inDegree per node.
  2. Initialize a queue with all nodes where inDegree == 0.
  3. Pop node, append to order; for each neighbor, decrement inDegree; if neighbor hits 0, enqueue.
  4. If order length equals total nodes, DAG OK; else cycle.

DFS 3-color

  1. Mark all white; define recursive visit(node).
  2. On entry: gray; for each edge node → next, if next is gray return cycle; if white, recurse.
  3. On exit: black; push node to finish list.
  4. Reverse finish list for topo order (or use prepend carefully).

Complexity

  • Time: O(V + E) for both Kahn and DFS over adjacency lists.
  • Space: O(V + E) for graph plus O(V) for queue/colors and output.

Generic Code Template

Go

func topologicalSortKahn(numNodes int, edges [][]int) ([]int, bool) {
	adj := make([][]int, numNodes)
	inDegree := make([]int, numNodes)
	for _, edge := range edges {
		from, to := edge[0], edge[1]
		adj[from] = append(adj[from], to)
		inDegree[to]++
	}
	queue := make([]int, 0)
	for node := 0; node < numNodes; node++ {
		if inDegree[node] == 0 {
			queue = append(queue, node)
		}
	}
	order := make([]int, 0, numNodes)
	head := 0
	for head < len(queue) {
		current := queue[head]
		head++
		order = append(order, current)
		for _, neighbor := range adj[current] {
			inDegree[neighbor]--
			if inDegree[neighbor] == 0 {
				queue = append(queue, neighbor)
			}
		}
	}
	if len(order) != numNodes {
		return nil, false
	}
	return order, true
}

const colorWhite = 0
const colorGray = 1
const colorBlack = 2

func topologicalSortDFS(numNodes int, edges [][]int) ([]int, bool) {
	adj := make([][]int, numNodes)
	for _, edge := range edges {
		from, to := edge[0], edge[1]
		adj[from] = append(adj[from], to)
	}
	color := make([]int, numNodes)
	finishOrder := make([]int, 0, numNodes)
	var hasCycle bool
	var visit func(int)
	visit = func(node int) {
		if hasCycle {
			return
		}
		color[node] = colorGray
		for _, neighbor := range adj[node] {
			if color[neighbor] == colorGray {
				hasCycle = true
				return
			}
			if color[neighbor] == colorWhite {
				visit(neighbor)
			}
		}
		color[node] = colorBlack
		finishOrder = append(finishOrder, node)
	}
	for node := 0; node < numNodes; node++ {
		if color[node] == colorWhite {
			visit(node)
		}
	}
	if hasCycle {
		return nil, false
	}
	for left, right := 0, len(finishOrder)-1; left < right; left, right = left+1, right-1 {
		finishOrder[left], finishOrder[right] = finishOrder[right], finishOrder[left]
	}
	return finishOrder, true
}

JavaScript

function topologicalSortKahn(numNodes, edges) {
  const adj = Array.from({ length: numNodes }, () => []);
  const inDegree = Array(numNodes).fill(0);
  for (const [from, to] of edges) {
    adj[from].push(to);
    inDegree[to] += 1;
  }
  const queue = [];
  for (let node = 0; node < numNodes; node++) {
    if (inDegree[node] === 0) queue.push(node);
  }
  const order = [];
  while (queue.length) {
    const current = queue.shift();
    order.push(current);
    for (const neighbor of adj[current]) {
      inDegree[neighbor] -= 1;
      if (inDegree[neighbor] === 0) queue.push(neighbor);
    }
  }
  if (order.length !== numNodes) return { order: null, ok: false };
  return { order, ok: true };
}

const COLOR_WHITE = 0;
const COLOR_GRAY = 1;
const COLOR_BLACK = 2;

function topologicalSortDFS(numNodes, edges) {
  const adj = Array.from({ length: numNodes }, () => []);
  for (const [from, to] of edges) adj[from].push(to);
  const color = Array(numNodes).fill(COLOR_WHITE);
  const finishOrder = [];
  let hasCycle = false;

  function visit(node) {
    if (hasCycle) return;
    color[node] = COLOR_GRAY;
    for (const neighbor of adj[node]) {
      if (color[neighbor] === COLOR_GRAY) {
        hasCycle = true;
        return;
      }
      if (color[neighbor] === COLOR_WHITE) visit(neighbor);
    }
    color[node] = COLOR_BLACK;
    finishOrder.push(node);
  }

  for (let node = 0; node < numNodes; node++) {
    if (color[node] === COLOR_WHITE) visit(node);
  }
  if (hasCycle) return { order: null, ok: false };
  finishOrder.reverse();
  return { order: finishOrder, ok: true };
}

Variants

  • Lexicographically smallest topo order: use min-heap instead of queue for Kahn.
  • All topological sorts: backtracking (exponential); rarely needed in interviews.
  • Alien dictionary: build graph between characters, topo sort; careful about cycles and implicit letters.

Representative Problems

Anti-patterns

  • Assuming one unique order; many valid answers unless constrained.
  • Using undirected union-find when problem is directed dependency (wrong model).
  • DFS topo without reversing finish times (order ends up backward).
  • Building graph with wrong edge direction (prerequisite vs dependent).

Last updated on

Spotted something unclear or wrong on this page?

On this page