Skip to content

Commit 67eebcb

Browse files
authored
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
1 parent 24c7f1f commit 67eebcb

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

graph/kahn.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Kahn's algorithm computes a topological ordering of a directed acyclic graph (DAG).
2+
// Time Complexity: O(V + E)
3+
// Space Complexity: O(V + E)
4+
// Reference: https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm
5+
// see graph.go, topological.go, kahn_test.go
6+
7+
package graph
8+
9+
// Kahn's algorithm computes a topological ordering of a directed acyclic graph (DAG).
10+
// `n` is the number of vertices,
11+
// `dependencies` is a list of directed edges, where each pair [a, b] represents
12+
// a directed edge from a to b (i.e. b depends on a).
13+
// Vertices are assumed to be labelled 0, 1, ..., n-1.
14+
// If the graph is not a DAG, the function returns nil.
15+
func Kahn(n int, dependencies [][]int) []int {
16+
g := Graph{vertices: n, Directed: true}
17+
// track the in-degree (number of incoming edges) of each vertex
18+
inDegree := make([]int, n)
19+
20+
// populate g with edges, increase the in-degree counts accordingly
21+
for _, d := range dependencies {
22+
// make sure we don't add the same edge twice
23+
if _, ok := g.edges[d[0]][d[1]]; !ok {
24+
g.AddEdge(d[0], d[1])
25+
inDegree[d[1]]++
26+
}
27+
}
28+
29+
// queue holds all vertices with in-degree 0
30+
// these vertices have no dependency and thus can be ordered first
31+
queue := make([]int, 0, n)
32+
33+
for i := 0; i < n; i++ {
34+
if inDegree[i] == 0 {
35+
queue = append(queue, i)
36+
}
37+
}
38+
39+
// order holds a valid topological order
40+
order := make([]int, 0, n)
41+
42+
// process the dependency-free vertices
43+
// every time we process a vertex, we "remove" it from the graph
44+
for len(queue) > 0 {
45+
// pop the first vertex from the queue
46+
vtx := queue[0]
47+
queue = queue[1:]
48+
// add the vertex to the topological order
49+
order = append(order, vtx)
50+
// "remove" all the edges coming out of this vertex
51+
// every time we remove an edge, the corresponding in-degree reduces by 1
52+
// if all dependencies on a vertex is removed, enqueue the vertex
53+
for neighbour := range g.edges[vtx] {
54+
inDegree[neighbour]--
55+
if inDegree[neighbour] == 0 {
56+
queue = append(queue, neighbour)
57+
}
58+
}
59+
}
60+
61+
// if the graph is a DAG, order should contain all the certices
62+
if len(order) != n {
63+
return nil
64+
}
65+
return order
66+
}

graph/kahn_test.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package graph
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestKahn(t *testing.T) {
8+
testCases := []struct {
9+
name string
10+
n int
11+
dependencies [][]int
12+
wantNil bool
13+
}{
14+
{
15+
"linear graph",
16+
3,
17+
[][]int{{0, 1}, {1, 2}},
18+
false,
19+
},
20+
{
21+
"diamond graph",
22+
4,
23+
[][]int{{0, 1}, {0, 2}, {1, 3}, {2, 3}},
24+
false,
25+
},
26+
{
27+
"star graph",
28+
5,
29+
[][]int{{0, 1}, {0, 2}, {0, 3}, {0, 4}},
30+
false,
31+
},
32+
{
33+
"disconnected graph",
34+
5,
35+
[][]int{{0, 1}, {0, 2}, {3, 4}},
36+
false,
37+
},
38+
{
39+
"cycle graph 1",
40+
4,
41+
[][]int{{0, 1}, {1, 2}, {2, 3}, {3, 0}},
42+
true,
43+
},
44+
{
45+
"cycle graph 2",
46+
4,
47+
[][]int{{0, 1}, {1, 2}, {2, 0}, {2, 3}},
48+
true,
49+
},
50+
{
51+
"single node graph",
52+
1,
53+
[][]int{},
54+
false,
55+
},
56+
{
57+
"empty graph",
58+
0,
59+
[][]int{},
60+
false,
61+
},
62+
{
63+
"redundant dependencies",
64+
4,
65+
[][]int{{0, 1}, {1, 2}, {1, 2}, {2, 3}},
66+
false,
67+
},
68+
{
69+
"island vertex",
70+
4,
71+
[][]int{{0, 1}, {0, 2}},
72+
false,
73+
},
74+
{
75+
"more complicated graph",
76+
14,
77+
[][]int{{1, 9}, {2, 0}, {3, 2}, {4, 5}, {4, 6}, {4, 7}, {6, 7},
78+
{7, 8}, {9, 4}, {10, 0}, {10, 1}, {10, 12}, {11, 13},
79+
{12, 0}, {12, 11}, {13, 5}},
80+
false,
81+
},
82+
}
83+
84+
for _, tc := range testCases {
85+
t.Run(tc.name, func(t *testing.T) {
86+
actual := Kahn(tc.n, tc.dependencies)
87+
if tc.wantNil {
88+
if actual != nil {
89+
t.Errorf("Kahn(%d, %v) = %v; want nil", tc.n, tc.dependencies, actual)
90+
}
91+
} else {
92+
if actual == nil {
93+
t.Errorf("Kahn(%d, %v) = nil; want valid order", tc.n, tc.dependencies)
94+
} else {
95+
seen := make([]bool, tc.n)
96+
positions := make([]int, tc.n)
97+
for i, v := range actual {
98+
seen[v] = true
99+
positions[v] = i
100+
}
101+
for i, v := range seen {
102+
if !v {
103+
t.Errorf("missing vertex %v", i)
104+
}
105+
}
106+
for _, d := range tc.dependencies {
107+
if positions[d[0]] > positions[d[1]] {
108+
t.Errorf("dependency %v not satisfied", d)
109+
}
110+
}
111+
}
112+
}
113+
})
114+
}
115+
}

0 commit comments

Comments
 (0)