From 8c73d121e41dea792286774cd959910ea556d461 Mon Sep 17 00:00:00 2001 From: Acid Date: Sat, 18 Jul 2026 17:34:33 -0400 Subject: [PATCH] new file: sets/stringsUnionFind.go --- sets/stringsUnionFind.go | 65 ++++++++ sets/unionFind.go | 11 +- tests/setsTests/stringsUnionFind_test.go | 193 +++++++++++++++++++++++ 3 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 sets/stringsUnionFind.go create mode 100644 tests/setsTests/stringsUnionFind_test.go diff --git a/sets/stringsUnionFind.go b/sets/stringsUnionFind.go new file mode 100644 index 0000000..0409271 --- /dev/null +++ b/sets/stringsUnionFind.go @@ -0,0 +1,65 @@ +package sets + +// NamedUnionFind{ +// unionFind *UnionFind +// index map[string]int +// names []string +// } +type NamedUnionFind struct { + unionFind *UnionFind + index map[string]int + names []string +} + +// NewStringsUnion() -> creates a disjoint set from a slice of strings, +// the constructor will deduplicate and return a NamedUnionFind +func NewStringsUnion(params []string) *NamedUnionFind { + namedUnion := &NamedUnionFind{ + index: make(map[string]int, len(params)), + } + + for _, name := range params { + if _, seen := namedUnion.index[name]; !seen { + namedUnion.index[name] = len(namedUnion.names) + namedUnion.names = append(namedUnion.names, name) + } + } + + namedUnion.unionFind = NewUnionFind(len(namedUnion.names)) + + return namedUnion +} + +// Union() -> creates an Union of 2 values, returns true if success +func (n *NamedUnionFind) Union(a, b string) bool { + paramA, ok1 := n.index[a] + paramB, ok2 := n.index[b] + if !ok1 || !ok2 { + return false + } + return n.unionFind.Union(paramA, paramB) +} + +// IsUnion() -> returns true if the parameters have the same root +func (n *NamedUnionFind) IsUnion(a, b string) bool { + paramA, ok1 := n.index[a] + paramB, ok2 := n.index[b] + if !ok1 || !ok2 { + return false + } + return n.unionFind.IsUnion(paramA, paramB) +} + +// Rep() -> returns the representative name of group, and false if the name is unknown. +func (n *NamedUnionFind) Rep(a string) (string, bool) { + ia, ok := n.index[a] + if !ok { + return "", false + } + return n.names[n.unionFind.Find(ia)], true +} + +// Disjointed() -> returns the count of disjointed sets remaining +func (n *NamedUnionFind) Disjointed() int { + return n.unionFind.Disjointed() +} diff --git a/sets/unionFind.go b/sets/unionFind.go index b5fce08..128508b 100644 --- a/sets/unionFind.go +++ b/sets/unionFind.go @@ -1,12 +1,17 @@ package sets +// UnionFind { +// disjointed int +// parent []int +// rank []int +// } type UnionFind struct { disjointed int parent []int rank []int } -// NewUnionFind() -> Creates an disjoint set +// NewUnionFind() -> Creates an disjoint set ,takes the unique elements as parameter. func NewUnionFind(uniqueElements int) *UnionFind { // counts from 0 correction uniqueElements += 1 @@ -68,7 +73,7 @@ func (uf *UnionFind) Union(a int, b int) bool { } // IsUnion() -> returns true if the parameters have the same root -func (uf UnionFind) IsUnion(a int, b int) bool { +func (uf *UnionFind) IsUnion(a int, b int) bool { if uf.Find(a) == uf.Find(b) { return true } @@ -76,6 +81,6 @@ func (uf UnionFind) IsUnion(a int, b int) bool { } // Disjointed() -> returns the count of disjointed sets remaining -func (uf UnionFind) Disjointed() int { +func (uf *UnionFind) Disjointed() int { return uf.disjointed } diff --git a/tests/setsTests/stringsUnionFind_test.go b/tests/setsTests/stringsUnionFind_test.go new file mode 100644 index 0000000..bd78094 --- /dev/null +++ b/tests/setsTests/stringsUnionFind_test.go @@ -0,0 +1,193 @@ +package tests + +import ( + "testing" + + "datastructures/sets" +) + +// --- NewStringsUnion --- + +func TestNewStringsUnionStartsFullyDisjointed(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c", "d"}) + if got := u.Disjointed(); got != 4 { + t.Errorf("expected 4 disjoint sets initially, got %d", got) + } +} + +func TestNewStringsUnionDeduplicates(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "a", "c", "b", "a"}) + // only a, b, c are unique + if got := u.Disjointed(); got != 3 { + t.Errorf("expected 3 disjoint sets after dedup, got %d", got) + } +} + +func TestNewStringsUnionEachNameIsOwnRep(t *testing.T) { + names := []string{"alice", "bob", "carol"} + u := sets.NewStringsUnion(names) + for _, name := range names { + rep, ok := u.Rep(name) + if !ok { + t.Errorf("expected %q to be known", name) + } + if rep != name { + t.Errorf("expected %q to be its own representative, got %q", name, rep) + } + } +} + +func TestNewStringsUnionEmptyInput(t *testing.T) { + u := sets.NewStringsUnion(nil) + if got := u.Disjointed(); got != 0 { + t.Errorf("expected 0 disjoint sets for empty input, got %d", got) + } +} + +// --- Union --- + +func TestNamedUnionReturnsTrueOnMerge(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c"}) + if !u.Union("a", "b") { + t.Error("expected Union(a,b) to return true on a fresh merge") + } +} + +func TestNamedUnionReturnsFalseWhenAlreadyJoined(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c"}) + u.Union("a", "b") + if u.Union("a", "b") { + t.Error("expected Union(a,b) to return false when already in the same set") + } +} + +func TestNamedUnionReturnsFalseTransitively(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c"}) + u.Union("a", "b") + u.Union("b", "c") + // a and c are now connected through b + if u.Union("a", "c") { + t.Error("expected Union(a,c) to return false (connected via b)") + } +} + +func TestNamedUnionUnknownNameReturnsFalse(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b"}) + if u.Union("a", "ghost") { + t.Error("expected Union with an unknown name to return false") + } + // the unknown name must not have silently merged anything + if got := u.Disjointed(); got != 2 { + t.Errorf("expected the count to stay at 2 after a failed union, got %d", got) + } +} + +func TestNamedUnionDecrementsDisjointedCount(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c", "d"}) + u.Union("a", "b") // 4 -> 3 + u.Union("c", "d") // 3 -> 2 + if got := u.Disjointed(); got != 2 { + t.Errorf("expected 2 disjoint sets, got %d", got) + } +} + +func TestNamedUnionIsSymmetric(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b"}) + u.Union("b", "a") + if !u.IsUnion("a", "b") { + t.Error("expected Union(b,a) to connect a and b regardless of argument order") + } +} + +// --- IsUnion --- + +func TestNamedIsUnionTrueAfterMerge(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c"}) + u.Union("a", "b") + if !u.IsUnion("a", "b") { + t.Error("expected a and b to be in the same set after Union") + } +} + +func TestNamedIsUnionTransitive(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c", "d"}) + u.Union("a", "b") + u.Union("b", "c") + if !u.IsUnion("a", "c") { + t.Error("expected a and c to be connected through b") + } + if u.IsUnion("a", "d") { + t.Error("expected d to remain disjoint from the {a,b,c} set") + } +} + +func TestNamedIsUnionUnknownNameReturnsFalse(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b"}) + if u.IsUnion("a", "ghost") { + t.Error("expected IsUnion with an unknown name to return false") + } +} + +// --- Rep --- + +func TestRepIsSharedAfterUnion(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c"}) + u.Union("a", "b") + u.Union("b", "c") + + repA, _ := u.Rep("a") + repB, _ := u.Rep("b") + repC, _ := u.Rep("c") + if repA != repB || repB != repC { + t.Errorf("expected a, b, c to share one representative, got %q %q %q", repA, repB, repC) + } +} + +func TestRepUnknownNameReturnsFalse(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b"}) + if rep, ok := u.Rep("ghost"); ok || rep != "" { + t.Errorf("expected (\"\", false) for an unknown name, got (%q, %v)", rep, ok) + } +} + +func TestRepDistinctForSeparateSets(t *testing.T) { + u := sets.NewStringsUnion([]string{"a", "b", "c", "d"}) + u.Union("a", "b") + u.Union("c", "d") + + repA, _ := u.Rep("a") + repC, _ := u.Rep("c") + if repA == repC { + t.Errorf("expected separate sets to have different representatives, both were %q", repA) + } +} + +// --- Scenario: grouping people into friend circles --- + +func TestFriendCircles(t *testing.T) { + friendships := [][2]string{ + {"alice", "bob"}, {"bob", "carol"}, // circle A: alice-bob-carol + {"dave", "erin"}, // circle B: dave-erin + {"frank", "grace"}, {"grace", "frank"}, // circle C: frank-grace (redundant) + } + + circles := sets.NewStringsUnion([]string{ + "alice", "bob", "carol", "dave", "erin", "frank", "grace", + }) + for _, f := range friendships { + circles.Union(f[0], f[1]) + } + + if got := circles.Disjointed(); got != 3 { + t.Errorf("expected 3 friend circles, got %d", got) + } + if !circles.IsUnion("alice", "carol") { + t.Error("expected alice and carol to be in the same circle") + } + if circles.IsUnion("alice", "dave") { + t.Error("expected alice (circle A) and dave (circle B) to be in different circles") + } + if !circles.IsUnion("frank", "grace") { + t.Error("expected frank and grace to be in the same circle despite the redundant link") + } +}