← Data structures from scratch · Lesson 27/32
What comes first: topological order
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.
$ 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.
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 becausekahnis 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.