Skip to content
Ayotl

← Data structures from scratch · Lesson 27/32

What comes first: topological order

✕
1/8

Dependencies as arrows

A made-up study plan: Programming I (P1) before Programming II (P2); P2 before Data Structures (ED); ED and Discrete Math (MD) before Algorithms (AL); and AL before Compilers (CO). Each "before" is an arrow.

A topological order is a line-up of all the vertices where every arrow points forward: nothing shows up before something it depends on. It only exists if the graph is directed and acyclic, a DAG (directed acyclic graph). If A needs B and B needs A, there's nowhere to start.

And it's rarely unique: MD can go first or right before AL.

P1P2EDMDALCO
leccion.js

$ node leccion.js

Press RUN (or Ctrl+Enter).

What to look at

  • The first two orders are valid even though they differ: MD can go in several places.
  • The third isn't: it puts Algorithms before Data Structures and Discrete Math.
The whole lesson on one page

Dependencies as arrows

A made-up study plan: Programming I (P1) before Programming II (P2); P2 before Data Structures (ED); ED and Discrete Math (MD) before Algorithms (AL); and AL before Compilers (CO). Each "before" is an arrow.

A topological order is a line-up of all the vertices where every arrow points forward: nothing shows up before something it depends on. It only exists if the graph is directed and acyclic, a DAG (directed acyclic graph). If A needs B and B needs A, there's nowhere to start.

And it's rarely unique: MD can go first or right before AL.

P1P2EDMDALCO
const requisitos = [["P1", "P2"], ["P2", "ED"], ["ED", "AL"], ["MD", "AL"], ["AL", "CO"]];

function esValido(orden) {
  const pos = {};
  orden.forEach((m, i) => { pos[m] = i; });
  // every "before -> after" arrow has to point forward
  return requisitos.every(([antes, despues]) => pos[antes] < pos[despues]);
}

console.log(esValido(["P1", "MD", "P2", "ED", "AL", "CO"]));
console.log(esValido(["MD", "P1", "P2", "ED", "AL", "CO"]));
console.log(esValido(["P1", "P2", "AL", "ED", "MD", "CO"]));
  • The first two orders are valid even though they differ: MD can go in several places.
  • The third isn't: it puts Algorithms before Data Structures and Discrete Math.

Kahn's algorithm

The clearest way to build it is to count each vertex's in-degree: how many arrows point at it, that is, how many things it's still waiting for.

  • The ones with degree 0 depend on nothing: they go into a queue.
  • Take one out, add it to the order and subtract 1 from each vertex it points to (that requirement is now met).
  • Whoever reaches 0 joins the queue.

If at the end the order has fewer vertices than the graph, the missing ones are stuck in a cycle: they never reached 0. It costs O(V + E).

const grafo = {
  P1: ["P2"],
  P2: ["ED"],
  ED: ["AL"],
  MD: ["AL"],
  AL: ["CO"],
  CO: [],
};

function kahn(grafo) {
  const grado = {};
  for (const v in grafo) grado[v] = 0;
  for (const v in grafo) for (const w of grafo[v]) grado[w]++;
  console.log("in-degrees:", grado);

  const cola = Object.keys(grafo).filter((v) => grado[v] === 0);
  const orden = [];
  let cabeza = 0;
  while (cabeza < cola.length) {
    const v = cola[cabeza++];
    orden.push(v);
    for (const w of grafo[v]) {
      grado[w]--;                      // one requirement fewer
      if (grado[w] === 0) cola.push(w);
    }
  }
  // if vertices are missing, they got stuck in a cycle
  return orden.length === Object.keys(grafo).length ? orden : null;
}

console.log(kahn(grafo));
console.log(kahn({ A: ["B"], B: ["C"], C: ["A"] }));
  • P1 and MD start at degree 0; AL starts at 2 (it's waiting on ED and MD).
  • Out comes [P1, MD, P2, ED, AL, CO]: AL only joins the queue once ED and MD are out.
  • In the triangle A → B → C → A nobody starts at 0: it returns null. (The degrees print twice because kahn is called twice.)

With DFS, and what it's for

Another way: a DFS that writes down each vertex when it finishes with everything that depends on it. That list, reversed, is a topological order. It gives a different but equally valid order (to detect cycles this way you need to tell "visiting" from "done", skipped here).

This shows up in more places than it seems: npm deciding what order to install packages in, make deciding what order to compile files in, a spreadsheet deciding what order to recalculate cells that depend on others, and a recipe deciding what order to do the steps in.

const grafo = {
  P1: ["P2"],
  P2: ["ED"],
  ED: ["AL"],
  MD: ["AL"],
  AL: ["CO"],
  CO: [],
};

function topoDFS(grafo) {
  const visto = new Set(), salida = [];
  function visitar(v) {
    if (visto.has(v)) return;
    visto.add(v);
    for (const w of grafo[v]) visitar(w);
    salida.push(v);   // finishes after everything that depends on it
  }
  for (const v in grafo) visitar(v);
  console.log("on finish:", salida);
  return salida.reverse();
}

console.log(topoDFS(grafo));
  • On finish, CO comes out first (nothing depends on it) and P1 near the end.
  • Reversed it's [MD, P1, P2, ED, AL, CO]: different from Kahn's and also valid.