Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions graph/kahn.go
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
}
115 changes: 115 additions & 0 deletions graph/kahn_test.go
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)
}
}
}
}
})
}
}
Loading