-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
1 parent
24c7f1f
commit 67eebcb
Showing
2 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
} |