Skip to content

feat: add Kahn's algorithm for topological sorting - #403

Open
Vir-007 wants to merge 1 commit into
bobluppes:mainfrom
Vir-007:issue-334
Open

feat: add Kahn's algorithm for topological sorting#403
Vir-007 wants to merge 1 commit into
bobluppes:mainfrom
Vir-007:issue-334

Conversation

@Vir-007

@Vir-007 Vir-007 commented Sep 12, 2026

Copy link
Copy Markdown

Fixes #334

What

Adds kahn_topological_sort, the BFS/in-degree based topological sort, alongside the existing DFS-based one under include/graaflib/algorithm/topological_sorting/. Follows the same header/.tpp split as its sibling.

template <typename V, typename E>
[[nodiscard]] std::vector<vertex_id_t> kahn_topological_sort(
    const graph<V, E, graph_type::DIRECTED>& graph);

Why

Kahn's algorithm is the standard alternative to the DFS approach and has properties the DFS version doesn't: it detects cycles as a side effect rather than needing a separate pass, it's iterative so it has no recursion-depth limit on deep graphs, and its queue contents form dependency "levels" that suit layered/parallel scheduling.

How

Vertices whose in-degree has dropped to zero are emitted from a queue, relaxing their outgoing edges as they go.

Cycle reporting — if not every vertex could be emitted, the remaining vertices lie on or downstream of a cycle and std::invalid_argument is thrown, mirroring how bellman_ford_shortest_paths reports a negative cycle (as the issue suggested). Self-loops are covered by the same check, since a self-loop contributes one to its own vertex's in-degree.

Worth flagging for review: this means the two topological sorts in the same directory report cycles differently — DFS returns std::nullopt, this one throws. I followed the issue's suggestion, but happy to switch to std::optional for symmetry with the sibling function if you'd prefer that.

In-degree computation — the issue suggested reusing properties::vertex_indegree "where convenient". It turned out not to be: that helper scans every vertex's neighbor set on each call, so calling it per vertex would make the sort O(|V| * (|V| + |E|)) and cost Kahn's algorithm its main selling point. In-degrees are instead counted in a single O(|V| + |E|) sweep over the adjacency lists. There's a comment in the code explaining this.

Docs

docs/src/algorithms/topological-sort/kahn.md covers the algorithm, complexity (O(|V| + |E|) time, O(|V|) memory), and a side-by-side comparison with the DFS-based version.

The section previously held a single flat page, so I added a README.md and nested both pages under it in SUMMARY.md, matching how cycle-detection, shortest-path etc. are laid out. The existing topological-sort.md keeps its path, so published links to it still work. Also updated the root README.md algorithm list and llms.txt.

Note: the issue referenced docs/docs/algorithms/topological-sort/, but docs now live under docs/src/, so I used the current path.

How tested

  • 10 new tests in test/graaflib/algorithm/topological_sorting/kahn_topological_sorting_test.cpp, mirroring the GoogleTest style of dfs_topological_sorting_test.cpp:
    • DAGs — chain, rhombus, multi-path, single vertex, empty graph
    • Cyclic — 4-cycle, self-loop, and a cycle in one component of an otherwise acyclic graph
    • Disconnected — two independent components, and a graph with fully isolated vertices
  • Because the algorithm picks its next source from an unordered container, several orderings are valid for most of these graphs. Rather than enumerating them, a helper asserts the two defining properties: the result is a permutation of the graph's vertices, and every edge points forward. Graphs with exactly one valid order (the chain) are still asserted exactly.
  • Full suite passes: 795 tests, 210 suites.
  • Mutation-checked the new tests — removing the cycle check, enqueuing neighbors before their in-degree reaches zero, and skipping the isolated-vertex seeding each fail the relevant tests (3, 2 and 6 respectively); all pass again once reverted.
  • clang-format clean against the repo's .clang-format.

One note on local verification: cmake isn't installed on my machine, so rather than cmake -B build && ctest I compiled the suite directly with g++ -std=c++20 against a system GoogleTest. The system fmt is 12.x while the build pins 9.1.0, and fmt 12 split fmt::format out of fmt/core.h, so the existing fmt-using tests needed -include fmt/format.h to compile. That's purely a local toolchain artifact — it doesn't touch this change, and nothing in this PR uses fmt. CI will exercise the real CMake path.

Graaf only implemented DFS-based topological sorting. This adds Kahn's
algorithm, the BFS/in-degree based alternative, alongside it.

The implementation emits vertices whose in-degree has dropped to zero,
relaxing outgoing edges as it goes. Cycles fall out of the algorithm:
if not every vertex could be emitted, the remaining ones lie on or
downstream of a cycle, and std::invalid_argument is thrown. This mirrors
how bellman_ford_shortest_paths reports a negative cycle. Self-loops are
covered, since a self-loop contributes one to its own vertex's in-degree.

In-degrees are counted in a single sweep over the adjacency lists rather
than via properties::vertex_indegree, which scans the whole graph on each
call and would make the sort O(|V| * (|V| + |E|)) instead of O(|V| + |E|).

Docs cover complexity and compare the two approaches side by side. The
topological-sort section gains a README so both pages nest under it; the
existing page keeps its URL.

Tests cover DAGs (chain, rhombus, multi-path), cyclic graphs (cycle,
self-loop, cycle in one component of several), and disconnected graphs
(two components, isolated vertices), plus empty and single-vertex graphs.
@github-actions

Copy link
Copy Markdown
Contributor

Hi there! Thank you for creating your first pull-request on the Graaf library :)

@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.81529% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.56%. Comparing base (600b02a) to head (d9c14ad).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...ological_sorting/kahn_topological_sorting_test.cpp 96.87% 4 Missing ⚠️
...m/topological_sorting/kahn_topological_sorting.tpp 96.55% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #403      +/-   ##
==========================================
- Coverage   99.71%   99.56%   -0.16%     
==========================================
  Files          60       62       +2     
  Lines        2823     2980     +157     
  Branches      151      167      +16     
==========================================
+ Hits         2815     2967     +152     
- Misses          8       13       +5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Kahn's algorithm for topological sorting

1 participant