Skip to content

Commit

Permalink
feat: add Kahn's algorithm for topological sort (#735)
Browse files Browse the repository at this point in the history
* feat: implemented kahn's algorithm

* doc: added doc for graph/kahn.go

* test: added tests for graph/kahn.go
  • Loading branch information
soondubu137 authored Sep 8, 2024
1 parent 24c7f1f commit 67eebcb
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 0 deletions.
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)
}
}
}
}
})
}
}

0 comments on commit 67eebcb

Please sign in to comment.