diff --git a/README.md b/README.md index 16986b9..65a62d2 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ ## Graph - [x] Unweighted -- [ ] Weighted +- [x] Weighted - [ ] Directed ## Sets diff --git a/graphs/weightedGraph.go b/graphs/weightedGraph.go new file mode 100644 index 0000000..b3277da --- /dev/null +++ b/graphs/weightedGraph.go @@ -0,0 +1,90 @@ +package graphs + +type Number interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | + ~float32 | ~float64 +} + +// Weighted Graph , undirected +type WeightedGraph[T comparable, W Number] struct { + adjList map[T]map[T]W +} + +// NewWeightedGraph() -> Creates a Weighted Graph. +// +// gg := graphs.NewWeightedGraph[string, int]() +func NewWeightedGraph[T comparable, W Number]() *WeightedGraph[T, W] { + gp := &WeightedGraph[T, W]{ + adjList: make(map[T]map[T]W), + } + + return gp +} + +// AddVertex() -> add vertex connected to vertex with weight +func (gp *WeightedGraph[T, W]) AddVertex(newVertex T, connectToVertex T, weight W) bool { + // early return + if newVertex == connectToVertex { + return false + } + + gp.AddIsolatedVertex(newVertex) + gp.AddIsolatedVertex(connectToVertex) + + _, exist := gp.adjList[newVertex][connectToVertex] + + if !exist { + gp.adjList[newVertex][connectToVertex] = weight + gp.adjList[connectToVertex][newVertex] = weight + return true + } + + return false +} + +// AddIsolatedVertex() -> Creates an isolated Vertex returns True if ok , +// false if vertex already exist +func (gp *WeightedGraph[T, W]) AddIsolatedVertex(newVertex T) bool { + _, exist := gp.adjList[newVertex] + if !exist { + gp.adjList[newVertex] = make(map[T]W) + return true + } + return false +} + +// ConnectEdge() -> Connects two already existing vertices with a weighted edge. +// Returns false if either vertex is missing or the edge is already there. +func (gp *WeightedGraph[T, W]) ConnectEdge(vertex T, to T, weight W) bool { + if _, exist := gp.adjList[vertex]; !exist { + return false + } + + if _, exist := gp.adjList[to]; !exist { + return false + } + + return gp.AddVertex(vertex, to, weight) +} + +// Display() +func (gp *WeightedGraph[T, W]) Display() map[T]map[T]W { + return gp.adjList +} + +// Order() -> returns the amount of Vertices +func (gp *WeightedGraph[T, W]) Order() int { + return len(gp.adjList) +} + +// Size() -> returns the amount of Edges +func (gp *WeightedGraph[T, W]) Size() int { + var size int + + for _, inner := range gp.adjList { + size += len(inner) + } + + return size / 2 +} diff --git a/tests/graphTests/weightedGraph_test.go b/tests/graphTests/weightedGraph_test.go new file mode 100644 index 0000000..7ff1bfd --- /dev/null +++ b/tests/graphTests/weightedGraph_test.go @@ -0,0 +1,252 @@ +package tests + +import ( + "testing" + + "datastructures/graphs" +) + +func TestNewWeightedGraphIsEmpty(t *testing.T) { + graph := graphs.NewWeightedGraph[int, int]() + + if graph.Order() != 0 { + t.Errorf("new graph Order = %d, want 0", graph.Order()) + } + if graph.Size() != 0 { + t.Errorf("new graph Size = %d, want 0", graph.Size()) + } +} + +func TestWeightedAddVertexCreatesBothAndConnects(t *testing.T) { + graph := graphs.NewWeightedGraph[string, int]() + + if !graph.AddVertex("a", "b", 4) { + t.Error("AddVertex on a new edge = false, want true") + } + + if graph.Order() != 2 { + t.Errorf("Order = %d, want 2", graph.Order()) + } + if graph.Size() != 1 { + t.Errorf("Size = %d, want 1", graph.Size()) + } + + adjacency := graph.Display() + if adjacency["a"]["b"] != 4 { + t.Errorf("weight a->b = %d, want 4", adjacency["a"]["b"]) + } + // undirected: the reverse direction carries the same weight + if adjacency["b"]["a"] != 4 { + t.Errorf("weight b->a = %d, want 4 (undirected)", adjacency["b"]["a"]) + } +} + +// Adding a second edge from the same vertex must not clobber the first. +func TestWeightedAddVertexKeepsPreviousEdges(t *testing.T) { + graph := graphs.NewWeightedGraph[string, int]() + graph.AddVertex("a", "b", 4) + graph.AddVertex("a", "c", 2) + graph.AddVertex("a", "d", 7) + + adjacency := graph.Display() + if len(adjacency["a"]) != 3 { + t.Errorf("expected vertex a to have degree 3, got %v", adjacency["a"]) + } + for neighbor, want := range map[string]int{"b": 4, "c": 2, "d": 7} { + if adjacency["a"][neighbor] != want { + t.Errorf("weight a->%s = %d, want %d", neighbor, adjacency["a"][neighbor], want) + } + } + + if graph.Order() != 4 { + t.Errorf("Order = %d, want 4", graph.Order()) + } + if graph.Size() != 3 { + t.Errorf("Size = %d, want 3", graph.Size()) + } +} + +func TestWeightedAddVertexRejectsDuplicateEdge(t *testing.T) { + graph := graphs.NewWeightedGraph[string, int]() + graph.AddVertex("a", "b", 4) + + if graph.AddVertex("a", "b", 99) { + t.Error("AddVertex on an existing edge = true, want false") + } + // the original weight is kept, not overwritten + if weight := graph.Display()["a"]["b"]; weight != 4 { + t.Errorf("weight a->b = %d, want 4 (existing weight kept)", weight) + } + if graph.Size() != 1 { + t.Errorf("Size = %d, want 1", graph.Size()) + } +} + +func TestWeightedSelfLoopIsRejected(t *testing.T) { + graph := graphs.NewWeightedGraph[int, int]() + + if graph.AddVertex(1, 1, 5) { + t.Error("AddVertex with a self-loop = true, want false") + } + if graph.Order() != 0 { + t.Errorf("self-loop should create no vertex, Order = %d", graph.Order()) + } + if graph.Size() != 0 { + t.Errorf("self-loop should add no edge, Size = %d", graph.Size()) + } + + // a real edge on the same vertex must still work + graph.AddVertex(1, 2, 5) + if graph.Size() != 1 { + t.Errorf("Size = %d, want 1 after a valid edge", graph.Size()) + } +} + +func TestWeightedAddIsolatedVertex(t *testing.T) { + graph := graphs.NewWeightedGraph[string, int]() + + if !graph.AddIsolatedVertex("solo") { + t.Error("AddIsolatedVertex on new vertex = false, want true") + } + if graph.AddIsolatedVertex("solo") { + t.Error("AddIsolatedVertex on existing vertex = true, want false") + } + + if graph.Order() != 1 { + t.Errorf("Order = %d, want 1", graph.Order()) + } + if graph.Size() != 0 { + t.Errorf("isolated vertex should add no edges, Size = %d", graph.Size()) + } + if len(graph.Display()["solo"]) != 0 { + t.Errorf("isolated vertex should have empty adjacency, got %v", graph.Display()["solo"]) + } +} + +// An existing vertex must not be reset when a later edge is added to it. +func TestWeightedAddVertexPreservesExistingVertex(t *testing.T) { + graph := graphs.NewWeightedGraph[string, int]() + graph.AddVertex("a", "b", 4) + graph.AddVertex("c", "a", 2) + + adjacency := graph.Display() + if len(adjacency["a"]) != 2 { + t.Errorf("expected vertex a to keep both edges, got %v", adjacency["a"]) + } + if adjacency["a"]["b"] != 4 { + t.Errorf("weight a->b = %d, want 4", adjacency["a"]["b"]) + } + if adjacency["a"]["c"] != 2 { + t.Errorf("weight a->c = %d, want 2", adjacency["a"]["c"]) + } +} + +func TestConnectEdgeAllExisting(t *testing.T) { + graph := graphs.NewWeightedGraph[int, int]() + graph.AddIsolatedVertex(1) + graph.AddIsolatedVertex(2) + + if !graph.ConnectEdge(1, 2, 8) { + t.Error("ConnectEdge with existing vertices = false, want true") + } + if graph.Size() != 1 { + t.Errorf("Size = %d, want 1", graph.Size()) + } + + adjacency := graph.Display() + if adjacency[1][2] != 8 { + t.Errorf("weight 1->2 = %d, want 8", adjacency[1][2]) + } + if adjacency[2][1] != 8 { + t.Errorf("weight 2->1 = %d, want 8 (undirected)", adjacency[2][1]) + } +} + +func TestConnectEdgeRejectsMissingVertex(t *testing.T) { + graph := graphs.NewWeightedGraph[int, int]() + graph.AddIsolatedVertex(1) + // 99 does not exist + + if graph.ConnectEdge(1, 99, 3) { + t.Error("ConnectEdge with a missing target = true, want false") + } + if graph.ConnectEdge(99, 1, 3) { + t.Error("ConnectEdge with a missing source = true, want false") + } + + if graph.Order() != 1 { + t.Errorf("ConnectEdge should not create vertices, Order = %d, want 1", graph.Order()) + } + if graph.Size() != 0 { + t.Errorf("no edges should be added on failure, Size = %d", graph.Size()) + } +} + +func TestConnectEdgeRejectsDuplicate(t *testing.T) { + graph := graphs.NewWeightedGraph[int, int]() + graph.AddIsolatedVertex(1) + graph.AddIsolatedVertex(2) + graph.ConnectEdge(1, 2, 8) + + if graph.ConnectEdge(1, 2, 99) { + t.Error("ConnectEdge on an existing edge = true, want false") + } + if weight := graph.Display()[1][2]; weight != 8 { + t.Errorf("weight 1->2 = %d, want 8 (existing weight kept)", weight) + } + if graph.Size() != 1 { + t.Errorf("Size = %d, want 1", graph.Size()) + } +} + +func TestWeightedOrderAndSize(t *testing.T) { + graph := graphs.NewWeightedGraph[string, int]() + graph.AddVertex("a", "b", 4) + graph.AddVertex("b", "c", 7) + graph.AddVertex("c", "a", 2) + + if graph.Order() != 3 { + t.Errorf("Order = %d, want 3 (triangle)", graph.Order()) + } + if graph.Size() != 3 { + t.Errorf("Size = %d, want 3 (triangle)", graph.Size()) + } +} + +// A missing edge reads as the zero weight, so existence needs the comma-ok form. +func TestWeightedMissingEdgeIsZero(t *testing.T) { + graph := graphs.NewWeightedGraph[string, int]() + graph.AddVertex("a", "b", 4) + + weight, exists := graph.Display()["a"]["z"] + if exists { + t.Error("lookup of a missing edge reported exists = true, want false") + } + if weight != 0 { + t.Errorf("missing edge weight = %d, want the zero value 0", weight) + } +} + +func TestWeightedFloatWeights(t *testing.T) { + graph := graphs.NewWeightedGraph[string, float64]() + graph.AddVertex("a", "b", 2.5) + + if weight := graph.Display()["a"]["b"]; weight != 2.5 { + t.Errorf("weight a->b = %v, want 2.5", weight) + } + if weight := graph.Display()["b"]["a"]; weight != 2.5 { + t.Errorf("weight b->a = %v, want 2.5 (undirected)", weight) + } +} + +// A defined type with a numeric underlying type satisfies the Number constraint. +type Distance float64 + +func TestWeightedNamedWeightType(t *testing.T) { + graph := graphs.NewWeightedGraph[string, Distance]() + graph.AddVertex("a", "b", Distance(1.5)) + + if weight := graph.Display()["a"]["b"]; weight != Distance(1.5) { + t.Errorf("weight a->b = %v, want 1.5", weight) + } +}