From eb383ae975a446315cd5728390b37baaed7486d9 Mon Sep 17 00:00:00 2001 From: Dominik Braun Date: Thu, 21 Jul 2022 20:22:13 +0200 Subject: [PATCH] Implement `AdjacencyList` method (#17) --- directed.go | 18 +++++++++++++ directed_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ graph.go | 7 +++++ undirected.go | 18 +++++++++++++ undirected_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+) diff --git a/directed.go b/directed.go index bcc40bea..dee1f333 100644 --- a/directed.go +++ b/directed.go @@ -400,6 +400,24 @@ func (d *directed[K, T]) ShortestPathByHashes(sourceHash, targetHash K) ([]K, er return path, nil } +func (d *directed[K, T]) AdjacencyList() map[K][]K { + adjacencyList := make(map[K][]K) + + // Create an entry for each vertex to guarantee that all vertices are contained and its + // adjacencies can be safely accessed without a preceding check. + for vertexHash := range d.vertices { + adjacencyList[vertexHash] = make([]K, 0) + } + + for vertex, outEdges := range d.outEdges { + for adjacencyHash := range outEdges { + adjacencyList[vertex] = append(adjacencyList[vertex], adjacencyHash) + } + } + + return adjacencyList +} + func (d *directed[K, T]) edgesAreEqual(a, b Edge[T]) bool { aSourceHash := d.hash(a.Source) aTargetHash := d.hash(a.Target) diff --git a/directed_test.go b/directed_test.go index e9785633..d06b17e9 100644 --- a/directed_test.go +++ b/directed_test.go @@ -620,6 +620,71 @@ func TestDirected_ShortestPathByHashes(t *testing.T) { } } +func TestDirected_AdjacencyList(t *testing.T) { + tests := map[string]struct { + vertices []int + edges []Edge[int] + expected map[int][]int + }{ + "Y-shaped graph": { + vertices: []int{1, 2, 3, 4}, + edges: []Edge[int]{ + {Source: 1, Target: 3}, + {Source: 2, Target: 3}, + {Source: 3, Target: 4}, + }, + expected: map[int][]int{ + 1: {3}, + 2: {3}, + 3: {4}, + 4: {}, + }, + }, + "diamond-shaped graph": { + vertices: []int{1, 2, 3, 4}, + edges: []Edge[int]{ + {Source: 1, Target: 2}, + {Source: 1, Target: 3}, + {Source: 2, Target: 4}, + {Source: 3, Target: 4}, + }, + expected: map[int][]int{ + 1: {2, 3}, + 2: {4}, + 3: {4}, + 4: {}, + }, + }, + } + + for name, test := range tests { + graph := newDirected(IntHash, &traits{}) + + for _, vertex := range test.vertices { + graph.Vertex(vertex) + } + + for _, edge := range test.edges { + if err := graph.WeightedEdge(edge.Source, edge.Target, edge.Weight); err != nil { + t.Fatalf("%s: failed to add edge: %s", name, err.Error()) + } + } + + adjacencyList := graph.AdjacencyList() + + for expectedVertex, expectedAdjacencies := range test.expected { + adjacencies, ok := adjacencyList[expectedVertex] + if !ok { + t.Errorf("%s: expected vertex %v does not exist in adjacency list", name, expectedVertex) + } + + if !slicesAreEqual(expectedAdjacencies, adjacencies) { + t.Errorf("%s: adjacency expectancy for vertex %v doesn't match: expected %v, got %v", name, expectedVertex, test.expected, adjacencies) + } + } + } +} + func TestDirected_edgesAreEqual(t *testing.T) { tests := map[string]struct { a Edge[int] diff --git a/graph.go b/graph.go index 7f270067..e6dc8f5e 100644 --- a/graph.go +++ b/graph.go @@ -154,6 +154,13 @@ type Graph[K comparable, T any] interface { // ShortestPathByHashes does the same as ShortestPath, but uses hash values to identify the // vertices. ShortestPathByHashes(sourceHash, targetHash K) ([]K, error) + + // AdjacencyList computes an adjacency list for all vertices in the graph and returns a map + // with all vertex hashes mapped against a list of their adjacency hashes. + // + // Since the AdjacencyList map contains all vertices, it is safe to check for the adjacencies + // of every vertex even if some vertices don't have any adjacencies. + AdjacencyList() map[K][]K } // Edge represents a graph edge with a source and target vertex as well as a weight, which has the diff --git a/undirected.go b/undirected.go index 168bd96a..d2972ed3 100644 --- a/undirected.go +++ b/undirected.go @@ -329,6 +329,24 @@ func (u *undirected[K, T]) ShortestPathByHashes(sourceHash, targetHash K) ([]K, return path, nil } +func (u *undirected[K, T]) AdjacencyList() map[K][]K { + adjacencyList := make(map[K][]K) + + // Create an entry for each vertex to guarantee that all vertices are contained and its + // adjacencies can be safely accessed without a preceding check. + for vertexHash := range u.vertices { + adjacencyList[vertexHash] = make([]K, 0) + } + + for vertex, outEdges := range u.outEdges { + for adjacencyHash := range outEdges { + adjacencyList[vertex] = append(adjacencyList[vertex], adjacencyHash) + } + } + + return adjacencyList +} + func (u *undirected[K, T]) edgesAreEqual(a, b Edge[T]) bool { aSourceHash := u.hash(a.Source) aTargetHash := u.hash(a.Target) diff --git a/undirected_test.go b/undirected_test.go index 6d56d06e..37cb7c57 100644 --- a/undirected_test.go +++ b/undirected_test.go @@ -639,6 +639,71 @@ func TestUndirected_ShortestPathByHashes(t *testing.T) { } } +func TestUndirected_AdjacencyList(t *testing.T) { + tests := map[string]struct { + vertices []int + edges []Edge[int] + expected map[int][]int + }{ + "Y-shaped graph": { + vertices: []int{1, 2, 3, 4}, + edges: []Edge[int]{ + {Source: 1, Target: 3}, + {Source: 2, Target: 3}, + {Source: 3, Target: 4}, + }, + expected: map[int][]int{ + 1: {3}, + 2: {3}, + 3: {1, 2, 4}, + 4: {3}, + }, + }, + "diamond-shaped graph": { + vertices: []int{1, 2, 3, 4}, + edges: []Edge[int]{ + {Source: 1, Target: 2}, + {Source: 1, Target: 3}, + {Source: 2, Target: 4}, + {Source: 3, Target: 4}, + }, + expected: map[int][]int{ + 1: {2, 3}, + 2: {1, 4}, + 3: {1, 4}, + 4: {2, 3}, + }, + }, + } + + for name, test := range tests { + graph := newUndirected(IntHash, &traits{}) + + for _, vertex := range test.vertices { + graph.Vertex(vertex) + } + + for _, edge := range test.edges { + if err := graph.WeightedEdge(edge.Source, edge.Target, edge.Weight); err != nil { + t.Fatalf("%s: failed to add edge: %s", name, err.Error()) + } + } + + adjacencyList := graph.AdjacencyList() + + for expectedVertex, expectedAdjacencies := range test.expected { + adjacencies, ok := adjacencyList[expectedVertex] + if !ok { + t.Errorf("%s: expected vertex %v does not exist in adjacency list", name, expectedVertex) + } + + if !slicesAreEqual(expectedAdjacencies, adjacencies) { + t.Errorf("%s: adjacency expectancy for vertex %v doesn't match: expected %v, got %v", name, expectedVertex, test.expected, adjacencies) + } + } + } +} + func TestUndirected_edgesAreEqual(t *testing.T) { tests := map[string]struct { a Edge[int]