added DFS AND BSF to graphs
Go / build (push) Successful in 33s

This commit is contained in:
Acid
2026-07-25 23:53:46 -04:00
parent 5bc2be9e03
commit ba54411627
3 changed files with 518 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
package graphs
import (
"datastructures/linear"
)
// DFS() -> Depth First traversal, returns ordered slice or nil if start point
// is invalid
func (graph *Graph[T]) DFS(start T) []T {
// sanity check
if _, ok := graph.adjList[start]; !ok {
return nil
}
visited := make(map[T]bool, len(graph.adjList))
ordered := make([]T, 0, graph.Order())
stack := linear.Stack[T]{}
stack.Push(start)
visited[start] = true
for stack.Size() > 0 {
current, err := stack.Pop()
if err != nil {
break
}
ordered = append(ordered, current)
for _, neighbor := range graph.adjList[current] {
if !visited[neighbor] {
visited[neighbor] = true
stack.Push(neighbor)
}
}
}
return ordered
}
// BFS() -> Breadth First traversal start, returning the
// vertices in visit order. Returns nil if start is not in the graph.
func (graph *Graph[T]) BFS(start T) []T {
// sanity check
if _, ok := graph.adjList[start]; !ok {
return nil
}
visited := make(map[T]bool, len(graph.adjList))
ordered := make([]T, 0, graph.Order())
queue := linear.Queue[T]{}
queue.Push(start)
visited[start] = true
for queue.Size() > 0 {
current, err := queue.Pop()
if err != nil {
break
}
ordered = append(ordered, current)
for _, neighbor := range graph.adjList[current] {
if !visited[neighbor] {
visited[neighbor] = true
queue.Push(neighbor)
}
}
}
return ordered
}
+203
View File
@@ -0,0 +1,203 @@
package tests
import (
"slices"
"testing"
"datastructures/graphs"
)
func TestBFSUnknownStartReturnsNil(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
if order := graph.BFS("zz"); order != nil {
t.Errorf("BFS from unknown vertex = %v, want nil", order)
}
}
func TestBFSEmptyGraphReturnsNil(t *testing.T) {
graph := graphs.NewGraph[int]()
if order := graph.BFS(1); order != nil {
t.Errorf("BFS on empty graph = %v, want nil", order)
}
}
func TestBFSIsolatedVertexVisitsOnlyItself(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddIsolatedVertex("lonely")
order := graph.BFS("lonely")
if !slices.Equal(order, []string{"lonely"}) {
t.Errorf("BFS = %v, want [lonely]", order)
}
}
func TestBFSPathGraphVisitsInOrder(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("b", "c")
graph.AddVertex("c", "d")
order := graph.BFS("a")
if !slices.Equal(order, []string{"a", "b", "c", "d"}) {
t.Errorf("BFS = %v, want [a b c d]", order)
}
}
func TestBFSVisitsLevelByLevel(t *testing.T) {
// a
// / \
// b c
// | |
// d e
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
graph.AddVertex("c", "e")
order := graph.BFS("a")
if !slices.Equal(order, []string{"a", "b", "c", "d", "e"}) {
t.Errorf("BFS = %v, want [a b c d e]", order)
}
}
func TestBFSFromNonRootStart(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
// b's adjacency is [a d], so a comes before d
order := graph.BFS("b")
if !slices.Equal(order, []string{"b", "a", "d", "c"}) {
t.Errorf("BFS from b = %v, want [b a d c]", order)
}
}
func TestBFSCycleTerminatesWithoutRepeats(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("b", "c")
graph.AddVertex("c", "a")
order := graph.BFS("a")
if len(order) != 3 {
t.Fatalf("BFS on triangle = %v, want 3 vertices", order)
}
assertNoDuplicates(t, order)
}
func TestBFSDenseGraphVisitsEachVertexOnce(t *testing.T) {
// K4: every vertex connected to every other
graph := graphs.NewGraph[int]()
for first := 1; first <= 4; first++ {
for second := first + 1; second <= 4; second++ {
graph.AddVertex(first, second)
}
}
order := graph.BFS(1)
if len(order) != graph.Order() {
t.Errorf("BFS visited %d vertices, want %d", len(order), graph.Order())
}
assertNoDuplicates(t, order)
}
func TestBFSSkipsDisconnectedComponent(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("x", "y")
order := graph.BFS("a")
if !slices.Equal(order, []string{"a", "b"}) {
t.Errorf("BFS = %v, want only the component of a: [a b]", order)
}
}
func TestBFSReachesEveryVertexInConnectedGraph(t *testing.T) {
graph := graphs.NewGraph[int]()
graph.AddVertex(1, 2)
graph.AddVertex(2, 3)
graph.AddVertex(3, 4)
graph.AddVertex(4, 1)
graph.AddVertex(2, 5)
order := graph.BFS(1)
if len(order) != graph.Order() {
t.Errorf("BFS visited %d vertices, want %d", len(order), graph.Order())
}
for vertex := range graph.Display() {
if !slices.Contains(order, vertex) {
t.Errorf("vertex %d was never visited, order = %v", vertex, order)
}
}
}
func TestBFSDistancesAreNonDecreasing(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
graph.AddVertex("c", "d")
graph.AddVertex("d", "e")
graph.AddVertex("e", "f")
order := graph.BFS("a")
// hop count from a, derived from the graph itself
distances := map[string]int{"a": 0, "b": 1, "c": 1, "d": 2, "e": 3, "f": 4}
previous := 0
for _, vertex := range order {
current, ok := distances[vertex]
if !ok {
t.Fatalf("BFS returned unexpected vertex %q", vertex)
}
if current < previous {
t.Errorf("BFS = %v: %q at distance %d visited after distance %d", order, vertex, current, previous)
}
previous = current
}
}
func TestBFSDoesNotMutateGraph(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("b", "c")
orderBefore, sizeBefore := graph.Order(), graph.Size()
graph.BFS("a")
if graph.Order() != orderBefore || graph.Size() != sizeBefore {
t.Errorf("after BFS Order/Size = %d/%d, want %d/%d",
graph.Order(), graph.Size(), orderBefore, sizeBefore)
}
}
func TestBFSIsRepeatable(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
first := graph.BFS("a")
second := graph.BFS("a")
if !slices.Equal(first, second) {
t.Errorf("BFS not repeatable: %v then %v", first, second)
}
}
func assertNoDuplicates[T comparable](t *testing.T, order []T) {
t.Helper()
seen := make(map[T]bool, len(order))
for _, vertex := range order {
if seen[vertex] {
t.Errorf("vertex %v visited more than once in %v", vertex, order)
}
seen[vertex] = true
}
}
+234
View File
@@ -0,0 +1,234 @@
package tests
import (
"slices"
"testing"
"datastructures/graphs"
)
// DFS marks a vertex visited when it is pushed, so the neighbours of a vertex
// come off the stack in reverse adjacency order: the last neighbour listed is
// explored first. The exact-order expectations below follow that rule.
func TestDFSUnknownStartReturnsNil(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
if order := graph.DFS("zz"); order != nil {
t.Errorf("DFS from unknown vertex = %v, want nil", order)
}
}
func TestDFSEmptyGraphReturnsNil(t *testing.T) {
graph := graphs.NewGraph[int]()
if order := graph.DFS(1); order != nil {
t.Errorf("DFS on empty graph = %v, want nil", order)
}
}
func TestDFSIsolatedVertexVisitsOnlyItself(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddIsolatedVertex("lonely")
order := graph.DFS("lonely")
if !slices.Equal(order, []string{"lonely"}) {
t.Errorf("DFS = %v, want [lonely]", order)
}
}
func TestDFSPathGraphVisitsInOrder(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("b", "c")
graph.AddVertex("c", "d")
order := graph.DFS("a")
if !slices.Equal(order, []string{"a", "b", "c", "d"}) {
t.Errorf("DFS = %v, want [a b c d]", order)
}
}
func TestDFSGoesDeepBeforeWide(t *testing.T) {
// a
// / \
// b c
// | |
// d e
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
graph.AddVertex("c", "e")
// c is pushed last, so its branch is exhausted (c, e) before b's
order := graph.DFS("a")
if !slices.Equal(order, []string{"a", "c", "e", "b", "d"}) {
t.Errorf("DFS = %v, want [a c e b d]", order)
}
}
func TestDFSFromNonRootStart(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
// b's adjacency is [a d], so d is popped first, then a and its branch
order := graph.DFS("b")
if !slices.Equal(order, []string{"b", "d", "a", "c"}) {
t.Errorf("DFS from b = %v, want [b d a c]", order)
}
}
func TestDFSCycleTerminatesWithoutRepeats(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("b", "c")
graph.AddVertex("c", "a")
order := graph.DFS("a")
if len(order) != 3 {
t.Fatalf("DFS on triangle = %v, want 3 vertices", order)
}
assertNoDuplicates(t, order)
}
func TestDFSDenseGraphVisitsEachVertexOnce(t *testing.T) {
// K4: every vertex connected to every other
graph := graphs.NewGraph[int]()
for first := 1; first <= 4; first++ {
for second := first + 1; second <= 4; second++ {
graph.AddVertex(first, second)
}
}
order := graph.DFS(1)
if len(order) != graph.Order() {
t.Errorf("DFS visited %d vertices, want %d", len(order), graph.Order())
}
assertNoDuplicates(t, order)
}
func TestDFSSkipsDisconnectedComponent(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("x", "y")
order := graph.DFS("a")
if !slices.Equal(order, []string{"a", "b"}) {
t.Errorf("DFS = %v, want only the component of a: [a b]", order)
}
}
func TestDFSReachesEveryVertexInConnectedGraph(t *testing.T) {
graph := graphs.NewGraph[int]()
graph.AddVertex(1, 2)
graph.AddVertex(2, 3)
graph.AddVertex(3, 4)
graph.AddVertex(4, 1)
graph.AddVertex(2, 5)
order := graph.DFS(1)
if len(order) != graph.Order() {
t.Errorf("DFS visited %d vertices, want %d", len(order), graph.Order())
}
for vertex := range graph.Display() {
if !slices.Contains(order, vertex) {
t.Errorf("vertex %d was never visited, order = %v", vertex, order)
}
}
}
func TestDFSOrderIsReachableByBacktracking(t *testing.T) {
// a graph with cross edges and two cycles, where a naive traversal could
// emit a vertex that is not reachable from the current path
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
graph.AddVertex("c", "d")
graph.AddVertex("d", "e")
graph.AddVertex("e", "f")
graph.AddVertex("f", "a")
graph.AddVertex("c", "f")
order := graph.DFS("a")
assertNoDuplicates(t, order)
if len(order) != graph.Order() {
t.Fatalf("DFS = %v, want all %d vertices", order, graph.Order())
}
assertValidDFSOrder(t, graph.Display(), order)
}
func TestDFSHandlesDeepChainWithoutRecursion(t *testing.T) {
// deep enough to blow a recursive implementation's stack budget
const length = 10000
graph := graphs.NewGraph[int]()
for vertex := 0; vertex < length-1; vertex++ {
graph.AddVertex(vertex, vertex+1)
}
order := graph.DFS(0)
if len(order) != length {
t.Fatalf("DFS visited %d vertices, want %d", len(order), length)
}
for index, vertex := range order {
if vertex != index {
t.Fatalf("DFS[%d] = %d, want %d", index, vertex, index)
}
}
}
func TestDFSDoesNotMutateGraph(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("b", "c")
orderBefore, sizeBefore := graph.Order(), graph.Size()
graph.DFS("a")
if graph.Order() != orderBefore || graph.Size() != sizeBefore {
t.Errorf("after DFS Order/Size = %d/%d, want %d/%d",
graph.Order(), graph.Size(), orderBefore, sizeBefore)
}
}
func TestDFSIsRepeatable(t *testing.T) {
graph := graphs.NewGraph[string]()
graph.AddVertex("a", "b")
graph.AddVertex("a", "c")
graph.AddVertex("b", "d")
first := graph.DFS("a")
second := graph.DFS("a")
if !slices.Equal(first, second) {
t.Errorf("DFS not repeatable: %v then %v", first, second)
}
}
// assertValidDFSOrder checks that order is a legal depth-first ordering: each
// vertex must attach to the deepest vertex on the current path that is adjacent
// to it, which is exactly what backtracking out of a dead end produces.
func assertValidDFSOrder[T comparable](t *testing.T, adjacency map[T][]T, order []T) {
t.Helper()
if len(order) == 0 {
return
}
path := []T{order[0]}
for _, vertex := range order[1:] {
for len(path) > 0 && !slices.Contains(adjacency[path[len(path)-1]], vertex) {
path = path[:len(path)-1]
}
if len(path) == 0 {
t.Fatalf("DFS = %v: %v is not adjacent to anything on the path back to the start", order, vertex)
}
path = append(path, vertex)
}
}