From 67eebcba7b384f69aa1e8c9d2bee48f9966953f0 Mon Sep 17 00:00:00 2001 From: Y L <86551259+soondubu137@users.noreply.github.com> Date: Sun, 8 Sep 2024 15:27:10 -0500 Subject: [PATCH] feat: add Kahn's algorithm for topological sort (#735) * feat: implemented kahn's algorithm * doc: added doc for graph/kahn.go * test: added tests for graph/kahn.go --- graph/kahn.go | 66 ++++++++++++++++++++++++++ graph/kahn_test.go | 115 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 graph/kahn.go create mode 100644 graph/kahn_test.go diff --git a/graph/kahn.go b/graph/kahn.go new file mode 100644 index 000000000..6f9d44d71 --- /dev/null +++ b/graph/kahn.go @@ -0,0 +1,66 @@ +// Kahn's algorithm computes a topological ordering of a directed acyclic graph (DAG). +// Time Complexity: O(V + E) +// Space Complexity: O(V + E) +// Reference: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm +// see graph.go, topological.go, kahn_test.go + +package graph + +// Kahn's algorithm computes a topological ordering of a directed acyclic graph (DAG). +// `n` is the number of vertices, +// `dependencies` is a list of directed edges, where each pair [a, b] represents +// a directed edge from a to b (i.e. b depends on a). +// Vertices are assumed to be labelled 0, 1, ..., n-1. +// If the graph is not a DAG, the function returns nil. +func Kahn(n int, dependencies [][]int) []int { + g := Graph{vertices: n, Directed: true} + // track the in-degree (number of incoming edges) of each vertex + inDegree := make([]int, n) + + // populate g with edges, increase the in-degree counts accordingly + for _, d := range dependencies { + // make sure we don't add the same edge twice + if _, ok := g.edges[d[0]][d[1]]; !ok { + g.AddEdge(d[0], d[1]) + inDegree[d[1]]++ + } + } + + // queue holds all vertices with in-degree 0 + // these vertices have no dependency and thus can be ordered first + queue := make([]int, 0, n) + + for i := 0; i < n; i++ { + if inDegree[i] == 0 { + queue = append(queue, i) + } + } + + // order holds a valid topological order + order := make([]int, 0, n) + + // process the dependency-free vertices + // every time we process a vertex, we "remove" it from the graph + for len(queue) > 0 { + // pop the first vertex from the queue + vtx := queue[0] + queue = queue[1:] + // add the vertex to the topological order + order = append(order, vtx) + // "remove" all the edges coming out of this vertex + // every time we remove an edge, the corresponding in-degree reduces by 1 + // if all dependencies on a vertex is removed, enqueue the vertex + for neighbour := range g.edges[vtx] { + inDegree[neighbour]-- + if inDegree[neighbour] == 0 { + queue = append(queue, neighbour) + } + } + } + + // if the graph is a DAG, order should contain all the certices + if len(order) != n { + return nil + } + return order +} diff --git a/graph/kahn_test.go b/graph/kahn_test.go new file mode 100644 index 000000000..71536b4f6 --- /dev/null +++ b/graph/kahn_test.go @@ -0,0 +1,115 @@ +package graph + +import ( + "testing" +) + +func TestKahn(t *testing.T) { + testCases := []struct { + name string + n int + dependencies [][]int + wantNil bool + }{ + { + "linear graph", + 3, + [][]int{{0, 1}, {1, 2}}, + false, + }, + { + "diamond graph", + 4, + [][]int{{0, 1}, {0, 2}, {1, 3}, {2, 3}}, + false, + }, + { + "star graph", + 5, + [][]int{{0, 1}, {0, 2}, {0, 3}, {0, 4}}, + false, + }, + { + "disconnected graph", + 5, + [][]int{{0, 1}, {0, 2}, {3, 4}}, + false, + }, + { + "cycle graph 1", + 4, + [][]int{{0, 1}, {1, 2}, {2, 3}, {3, 0}}, + true, + }, + { + "cycle graph 2", + 4, + [][]int{{0, 1}, {1, 2}, {2, 0}, {2, 3}}, + true, + }, + { + "single node graph", + 1, + [][]int{}, + false, + }, + { + "empty graph", + 0, + [][]int{}, + false, + }, + { + "redundant dependencies", + 4, + [][]int{{0, 1}, {1, 2}, {1, 2}, {2, 3}}, + false, + }, + { + "island vertex", + 4, + [][]int{{0, 1}, {0, 2}}, + false, + }, + { + "more complicated graph", + 14, + [][]int{{1, 9}, {2, 0}, {3, 2}, {4, 5}, {4, 6}, {4, 7}, {6, 7}, + {7, 8}, {9, 4}, {10, 0}, {10, 1}, {10, 12}, {11, 13}, + {12, 0}, {12, 11}, {13, 5}}, + false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := Kahn(tc.n, tc.dependencies) + if tc.wantNil { + if actual != nil { + t.Errorf("Kahn(%d, %v) = %v; want nil", tc.n, tc.dependencies, actual) + } + } else { + if actual == nil { + t.Errorf("Kahn(%d, %v) = nil; want valid order", tc.n, tc.dependencies) + } else { + seen := make([]bool, tc.n) + positions := make([]int, tc.n) + for i, v := range actual { + seen[v] = true + positions[v] = i + } + for i, v := range seen { + if !v { + t.Errorf("missing vertex %v", i) + } + } + for _, d := range tc.dependencies { + if positions[d[0]] > positions[d[1]] { + t.Errorf("dependency %v not satisfied", d) + } + } + } + } + }) + } +}