topologicalSort.js 2.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. /**
  6. * Topologically sort `nodes` using Kahn's algorithm with source-order
  7. * tie-breaking. Nodes that participate in a cycle remain unvisited —
  8. * `visit` is never called for them — so the caller can naturally keep
  9. * them in their original position by treating "no visit" as "keep
  10. * source order".
  11. *
  12. * Precondition: every node appearing in `graph` (as a key OR inside any
  13. * successor set) must also appear in `nodes`. The caller owns this
  14. * invariant; the function does not validate it.
  15. *
  16. * Complexity: O(V·(V + E)). Each outer iteration scans the ready set
  17. * linearly to find the smallest source-index node. CSS composes graphs
  18. * are small (a handful of files per module) so this is fine; if a much
  19. * larger graph ever needs sorting here, swap in a min-heap.
  20. * @template T
  21. * @param {Map<T, Set<T>>} graph adjacency list (`a -> b` means `a` must come before `b`)
  22. * @param {T[]} nodes nodes in source first-appearance order
  23. * @param {(node: T, index: number) => void} visit called once per non-cyclic node in topological order
  24. * @returns {void}
  25. */
  26. module.exports = (graph, nodes, visit) => {
  27. /** @type {Map<T, number>} */
  28. const inDegree = new Map();
  29. /** @type {Map<T, number>} */
  30. const sourceIndex = new Map();
  31. for (let i = 0; i < nodes.length; i++) {
  32. inDegree.set(nodes[i], 0);
  33. sourceIndex.set(nodes[i], i);
  34. }
  35. for (const successors of graph.values()) {
  36. for (const to of successors) {
  37. inDegree.set(to, /** @type {number} */ (inDegree.get(to)) + 1);
  38. }
  39. }
  40. const ready = nodes.filter((n) => inDegree.get(n) === 0);
  41. let index = 0;
  42. while (ready.length > 0) {
  43. // Smallest-source-index wins ties. Linear scan + swap-with-last
  44. // + pop avoids re-sorting the ready set on every iteration.
  45. let minIdx = 0;
  46. for (let i = 1; i < ready.length; i++) {
  47. if (
  48. /** @type {number} */ (sourceIndex.get(ready[i])) <
  49. /** @type {number} */ (sourceIndex.get(ready[minIdx]))
  50. ) {
  51. minIdx = i;
  52. }
  53. }
  54. const node = ready[minIdx];
  55. ready[minIdx] = ready[ready.length - 1];
  56. ready.pop();
  57. visit(node, index++);
  58. const successors = graph.get(node);
  59. if (!successors) continue;
  60. for (const to of successors) {
  61. const newDeg = /** @type {number} */ (inDegree.get(to)) - 1;
  62. inDegree.set(to, newDeg);
  63. if (newDeg === 0) ready.push(to);
  64. }
  65. }
  66. };