From dbc9534ae75bb761cebaf2090bdb755869c280b5 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 10:41:48 +0200 Subject: [PATCH 1/4] feat(graph): add DSU-based account merge algorithm --- .../com/thealgorithms/graph/AccountMerge.java | 103 ++++++++++++++++++ .../thealgorithms/graph/AccountMergeTest.java | 46 ++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/main/java/com/thealgorithms/graph/AccountMerge.java create mode 100644 src/test/java/com/thealgorithms/graph/AccountMergeTest.java diff --git a/src/main/java/com/thealgorithms/graph/AccountMerge.java b/src/main/java/com/thealgorithms/graph/AccountMerge.java new file mode 100644 index 000000000000..bbe9c01a904f --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/AccountMerge.java @@ -0,0 +1,103 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Merges account records using Disjoint Set Union (Union-Find) on shared emails. + * + *

Input format: each account is a list where the first element is the user name and the + * remaining elements are emails. + */ +public final class AccountMerge { + private AccountMerge() { + } + + public static List> mergeAccounts(List> accounts) { + if (accounts == null || accounts.isEmpty()) { + return List.of(); + } + + UnionFind dsu = new UnionFind(accounts.size()); + Map emailToAccount = new HashMap<>(); + + for (int i = 0; i < accounts.size(); i++) { + List account = accounts.get(i); + for (int j = 1; j < account.size(); j++) { + String email = account.get(j); + Integer previous = emailToAccount.putIfAbsent(email, i); + if (previous != null) { + dsu.union(i, previous); + } + } + } + + Map> rootToEmails = new LinkedHashMap<>(); + for (Map.Entry entry : emailToAccount.entrySet()) { + int root = dsu.find(entry.getValue()); + rootToEmails.computeIfAbsent(root, ignored -> new ArrayList<>()).add(entry.getKey()); + } + + List> merged = new ArrayList<>(); + for (Map.Entry> entry : rootToEmails.entrySet()) { + int root = entry.getKey(); + List emails = entry.getValue(); + Collections.sort(emails); + + List mergedAccount = new ArrayList<>(); + mergedAccount.add(accounts.get(root).getFirst()); + mergedAccount.addAll(emails); + merged.add(mergedAccount); + } + + merged.sort((a, b) -> { + int cmp = a.getFirst().compareTo(b.getFirst()); + if (cmp != 0) { + return cmp; + } + return a.get(1).compareTo(b.get(1)); + }); + return merged; + } + + private static final class UnionFind { + private final int[] parent; + private final int[] rank; + + private UnionFind(int size) { + this.parent = new int[size]; + this.rank = new int[size]; + for (int i = 0; i < size; i++) { + parent[i] = i; + } + } + + private int find(int x) { + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } + + private void union(int x, int y) { + int rootX = find(x); + int rootY = find(y); + if (rootX == rootY) { + return; + } + + if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + } else if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } else { + parent[rootY] = rootX; + rank[rootX]++; + } + } + } +} diff --git a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java new file mode 100644 index 000000000000..9508d3e07944 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java @@ -0,0 +1,46 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class AccountMergeTest { + + @Test + void testMergeAccountsWithSharedEmails() { + List> accounts = List.of( + List.of("abc", "abc@mail.com", "abx@mail.com"), + List.of("abc", "abc@mail.com", "aby@mail.com"), + List.of("Mary", "mary@mail.com"), + List.of("John", "johnnybravo@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + + List> expected = List.of( + List.of("John", "johnnybravo@mail.com"), + List.of("Mary", "mary@mail.com"), + List.of("abc", "abc@mail.com", "abx@mail.com", "aby@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testAccountsWithSameNameButNoSharedEmailStaySeparate() { + List> accounts = List.of( + List.of("Alex", "alex1@mail.com"), + List.of("Alex", "alex2@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + List> expected = List.of( + List.of("Alex", "alex1@mail.com"), + List.of("Alex", "alex2@mail.com")); + + assertEquals(expected, merged); + } + + @Test + void testEmptyInput() { + assertEquals(List.of(), AccountMerge.mergeAccounts(List.of())); + } +} From 29137c95be0021c7c817d79871b06a319233823a Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 11:11:57 +0200 Subject: [PATCH 2/4] test(graph): add null and transitive account merge cases --- .../thealgorithms/graph/AccountMergeTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java index 9508d3e07944..0804d7fe6189 100644 --- a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java +++ b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java @@ -43,4 +43,24 @@ void testAccountsWithSameNameButNoSharedEmailStaySeparate() { void testEmptyInput() { assertEquals(List.of(), AccountMerge.mergeAccounts(List.of())); } + + @Test + void testNullInput() { + assertEquals(List.of(), AccountMerge.mergeAccounts(null)); + } + + @Test + void testTransitiveMergeAndDuplicateEmails() { + List> accounts = List.of( + List.of("A", "a1@mail.com", "a2@mail.com"), + List.of("A", "a2@mail.com", "a3@mail.com"), + List.of("A", "a3@mail.com", "a4@mail.com", "a4@mail.com")); + + List> merged = AccountMerge.mergeAccounts(accounts); + + List> expected = List.of( + List.of("A", "a1@mail.com", "a2@mail.com", "a3@mail.com", "a4@mail.com")); + + assertEquals(expected, merged); + } } From 28f3f7a7015b700e7d98aff1a6dc1979dfd5db08 Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 12:20:05 +0200 Subject: [PATCH 3/4] Handle no-email accounts in account merge --- .../com/thealgorithms/graph/AccountMerge.java | 9 +++++++++ .../thealgorithms/graph/AccountMergeTest.java | 16 ++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/main/java/com/thealgorithms/graph/AccountMerge.java b/src/main/java/com/thealgorithms/graph/AccountMerge.java index bbe9c01a904f..cf934a72eb68 100644 --- a/src/main/java/com/thealgorithms/graph/AccountMerge.java +++ b/src/main/java/com/thealgorithms/graph/AccountMerge.java @@ -41,6 +41,12 @@ public static List> mergeAccounts(List> accounts) { int root = dsu.find(entry.getValue()); rootToEmails.computeIfAbsent(root, ignored -> new ArrayList<>()).add(entry.getKey()); } + for (int i = 0; i < accounts.size(); i++) { + if (accounts.get(i).size() <= 1) { + int root = dsu.find(i); + rootToEmails.computeIfAbsent(root, ignored -> new ArrayList<>()); + } + } List> merged = new ArrayList<>(); for (Map.Entry> entry : rootToEmails.entrySet()) { @@ -59,6 +65,9 @@ public static List> mergeAccounts(List> accounts) { if (cmp != 0) { return cmp; } + if (a.size() == 1 || b.size() == 1) { + return Integer.compare(a.size(), b.size()); + } return a.get(1).compareTo(b.get(1)); }); return merged; diff --git a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java index 0804d7fe6189..0bc58905ca6c 100644 --- a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java +++ b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java @@ -63,4 +63,20 @@ void testTransitiveMergeAndDuplicateEmails() { assertEquals(expected, merged); } + + @Test + void testAccountsWithNoEmailsArePreserved() { + List> accounts = List.of( + List.of("Alex"), + List.of("Alex", "alex1@mail.com"), + List.of("Bob")); + + List> merged = AccountMerge.mergeAccounts(accounts); + List> expected = List.of( + List.of("Alex"), + List.of("Alex", "alex1@mail.com"), + List.of("Bob")); + + assertEquals(expected, merged); + } } From fc851fba112f61e41354d9d0efa9d3e26581a9ae Mon Sep 17 00:00:00 2001 From: nickzerjeski Date: Mon, 13 Apr 2026 12:31:00 +0200 Subject: [PATCH 4/4] Apply clang-format style to account merge tests --- .../thealgorithms/graph/AccountMergeTest.java | 37 ++++--------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java index 0bc58905ca6c..291be677d894 100644 --- a/src/test/java/com/thealgorithms/graph/AccountMergeTest.java +++ b/src/test/java/com/thealgorithms/graph/AccountMergeTest.java @@ -9,32 +9,21 @@ class AccountMergeTest { @Test void testMergeAccountsWithSharedEmails() { - List> accounts = List.of( - List.of("abc", "abc@mail.com", "abx@mail.com"), - List.of("abc", "abc@mail.com", "aby@mail.com"), - List.of("Mary", "mary@mail.com"), - List.of("John", "johnnybravo@mail.com")); + List> accounts = List.of(List.of("abc", "abc@mail.com", "abx@mail.com"), List.of("abc", "abc@mail.com", "aby@mail.com"), List.of("Mary", "mary@mail.com"), List.of("John", "johnnybravo@mail.com")); List> merged = AccountMerge.mergeAccounts(accounts); - List> expected = List.of( - List.of("John", "johnnybravo@mail.com"), - List.of("Mary", "mary@mail.com"), - List.of("abc", "abc@mail.com", "abx@mail.com", "aby@mail.com")); + List> expected = List.of(List.of("John", "johnnybravo@mail.com"), List.of("Mary", "mary@mail.com"), List.of("abc", "abc@mail.com", "abx@mail.com", "aby@mail.com")); assertEquals(expected, merged); } @Test void testAccountsWithSameNameButNoSharedEmailStaySeparate() { - List> accounts = List.of( - List.of("Alex", "alex1@mail.com"), - List.of("Alex", "alex2@mail.com")); + List> accounts = List.of(List.of("Alex", "alex1@mail.com"), List.of("Alex", "alex2@mail.com")); List> merged = AccountMerge.mergeAccounts(accounts); - List> expected = List.of( - List.of("Alex", "alex1@mail.com"), - List.of("Alex", "alex2@mail.com")); + List> expected = List.of(List.of("Alex", "alex1@mail.com"), List.of("Alex", "alex2@mail.com")); assertEquals(expected, merged); } @@ -51,31 +40,21 @@ void testNullInput() { @Test void testTransitiveMergeAndDuplicateEmails() { - List> accounts = List.of( - List.of("A", "a1@mail.com", "a2@mail.com"), - List.of("A", "a2@mail.com", "a3@mail.com"), - List.of("A", "a3@mail.com", "a4@mail.com", "a4@mail.com")); + List> accounts = List.of(List.of("A", "a1@mail.com", "a2@mail.com"), List.of("A", "a2@mail.com", "a3@mail.com"), List.of("A", "a3@mail.com", "a4@mail.com", "a4@mail.com")); List> merged = AccountMerge.mergeAccounts(accounts); - List> expected = List.of( - List.of("A", "a1@mail.com", "a2@mail.com", "a3@mail.com", "a4@mail.com")); + List> expected = List.of(List.of("A", "a1@mail.com", "a2@mail.com", "a3@mail.com", "a4@mail.com")); assertEquals(expected, merged); } @Test void testAccountsWithNoEmailsArePreserved() { - List> accounts = List.of( - List.of("Alex"), - List.of("Alex", "alex1@mail.com"), - List.of("Bob")); + List> accounts = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob")); List> merged = AccountMerge.mergeAccounts(accounts); - List> expected = List.of( - List.of("Alex"), - List.of("Alex", "alex1@mail.com"), - List.of("Bob")); + List> expected = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob")); assertEquals(expected, merged); }