Skip to content

Commit dbc9534

Browse files
committed
feat(graph): add DSU-based account merge algorithm
1 parent 1edf319 commit dbc9534

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.thealgorithms.graph;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.HashMap;
6+
import java.util.LinkedHashMap;
7+
import java.util.List;
8+
import java.util.Map;
9+
10+
/**
11+
* Merges account records using Disjoint Set Union (Union-Find) on shared emails.
12+
*
13+
* <p>Input format: each account is a list where the first element is the user name and the
14+
* remaining elements are emails.
15+
*/
16+
public final class AccountMerge {
17+
private AccountMerge() {
18+
}
19+
20+
public static List<List<String>> mergeAccounts(List<List<String>> accounts) {
21+
if (accounts == null || accounts.isEmpty()) {
22+
return List.of();
23+
}
24+
25+
UnionFind dsu = new UnionFind(accounts.size());
26+
Map<String, Integer> emailToAccount = new HashMap<>();
27+
28+
for (int i = 0; i < accounts.size(); i++) {
29+
List<String> account = accounts.get(i);
30+
for (int j = 1; j < account.size(); j++) {
31+
String email = account.get(j);
32+
Integer previous = emailToAccount.putIfAbsent(email, i);
33+
if (previous != null) {
34+
dsu.union(i, previous);
35+
}
36+
}
37+
}
38+
39+
Map<Integer, List<String>> rootToEmails = new LinkedHashMap<>();
40+
for (Map.Entry<String, Integer> entry : emailToAccount.entrySet()) {
41+
int root = dsu.find(entry.getValue());
42+
rootToEmails.computeIfAbsent(root, ignored -> new ArrayList<>()).add(entry.getKey());
43+
}
44+
45+
List<List<String>> merged = new ArrayList<>();
46+
for (Map.Entry<Integer, List<String>> entry : rootToEmails.entrySet()) {
47+
int root = entry.getKey();
48+
List<String> emails = entry.getValue();
49+
Collections.sort(emails);
50+
51+
List<String> mergedAccount = new ArrayList<>();
52+
mergedAccount.add(accounts.get(root).getFirst());
53+
mergedAccount.addAll(emails);
54+
merged.add(mergedAccount);
55+
}
56+
57+
merged.sort((a, b) -> {
58+
int cmp = a.getFirst().compareTo(b.getFirst());
59+
if (cmp != 0) {
60+
return cmp;
61+
}
62+
return a.get(1).compareTo(b.get(1));
63+
});
64+
return merged;
65+
}
66+
67+
private static final class UnionFind {
68+
private final int[] parent;
69+
private final int[] rank;
70+
71+
private UnionFind(int size) {
72+
this.parent = new int[size];
73+
this.rank = new int[size];
74+
for (int i = 0; i < size; i++) {
75+
parent[i] = i;
76+
}
77+
}
78+
79+
private int find(int x) {
80+
if (parent[x] != x) {
81+
parent[x] = find(parent[x]);
82+
}
83+
return parent[x];
84+
}
85+
86+
private void union(int x, int y) {
87+
int rootX = find(x);
88+
int rootY = find(y);
89+
if (rootX == rootY) {
90+
return;
91+
}
92+
93+
if (rank[rootX] < rank[rootY]) {
94+
parent[rootX] = rootY;
95+
} else if (rank[rootX] > rank[rootY]) {
96+
parent[rootY] = rootX;
97+
} else {
98+
parent[rootY] = rootX;
99+
rank[rootX]++;
100+
}
101+
}
102+
}
103+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.thealgorithms.graph;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import java.util.List;
6+
import org.junit.jupiter.api.Test;
7+
8+
class AccountMergeTest {
9+
10+
@Test
11+
void testMergeAccountsWithSharedEmails() {
12+
List<List<String>> accounts = List.of(
13+
List.of("abc", "abc@mail.com", "abx@mail.com"),
14+
List.of("abc", "abc@mail.com", "aby@mail.com"),
15+
List.of("Mary", "mary@mail.com"),
16+
List.of("John", "johnnybravo@mail.com"));
17+
18+
List<List<String>> merged = AccountMerge.mergeAccounts(accounts);
19+
20+
List<List<String>> expected = List.of(
21+
List.of("John", "johnnybravo@mail.com"),
22+
List.of("Mary", "mary@mail.com"),
23+
List.of("abc", "abc@mail.com", "abx@mail.com", "aby@mail.com"));
24+
25+
assertEquals(expected, merged);
26+
}
27+
28+
@Test
29+
void testAccountsWithSameNameButNoSharedEmailStaySeparate() {
30+
List<List<String>> accounts = List.of(
31+
List.of("Alex", "alex1@mail.com"),
32+
List.of("Alex", "alex2@mail.com"));
33+
34+
List<List<String>> merged = AccountMerge.mergeAccounts(accounts);
35+
List<List<String>> expected = List.of(
36+
List.of("Alex", "alex1@mail.com"),
37+
List.of("Alex", "alex2@mail.com"));
38+
39+
assertEquals(expected, merged);
40+
}
41+
42+
@Test
43+
void testEmptyInput() {
44+
assertEquals(List.of(), AccountMerge.mergeAccounts(List.of()));
45+
}
46+
}

0 commit comments

Comments
 (0)