From ba544116272c4cd1b9147835e037867190a692b5 Mon Sep 17 00:00:00 2001 From: Acid Date: Sat, 25 Jul 2026 23:53:46 -0400 Subject: [PATCH] added DFS AND BSF to graphs --- graphs/algorithms.go | 81 ++++++++++++ tests/graphTests/bfs_test.go | 203 ++++++++++++++++++++++++++++++ tests/graphTests/dfs_test.go | 234 +++++++++++++++++++++++++++++++++++ 3 files changed, 518 insertions(+) create mode 100644 graphs/algorithms.go create mode 100644 tests/graphTests/bfs_test.go create mode 100644 tests/graphTests/dfs_test.go diff --git a/graphs/algorithms.go b/graphs/algorithms.go new file mode 100644 index 0000000..bc737e7 --- /dev/null +++ b/graphs/algorithms.go @@ -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 +} diff --git a/tests/graphTests/bfs_test.go b/tests/graphTests/bfs_test.go new file mode 100644 index 0000000..3296575 --- /dev/null +++ b/tests/graphTests/bfs_test.go @@ -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 + } +} diff --git a/tests/graphTests/dfs_test.go b/tests/graphTests/dfs_test.go new file mode 100644 index 0000000..9b18418 --- /dev/null +++ b/tests/graphTests/dfs_test.go @@ -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) + } +}