Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions src/main/java/com/thealgorithms/graph/AccountMerge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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.
*
* <p>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<List<String>> mergeAccounts(List<List<String>> accounts) {
if (accounts == null || accounts.isEmpty()) {
return List.of();
}

UnionFind dsu = new UnionFind(accounts.size());
Map<String, Integer> emailToAccount = new HashMap<>();

for (int i = 0; i < accounts.size(); i++) {
List<String> 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<Integer, List<String>> rootToEmails = new LinkedHashMap<>();
for (Map.Entry<String, Integer> entry : emailToAccount.entrySet()) {
int root = dsu.find(entry.getValue());
rootToEmails.computeIfAbsent(root, ignored -> new ArrayList<>()).add(entry.getKey());
}
Comment on lines +39 to +43
Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accounts that contain only a name (i.e., no emails) are currently omitted from the output. rootToEmails is populated only by iterating emailToAccount, so accounts with an empty email list never become a root entry and are dropped. Consider explicitly handling accounts with account.size() <= 1 (or no emails after filtering) so they are returned as standalone merged accounts, and add a test for this case.

Copilot uses AI. Check for mistakes.
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<List<String>> merged = new ArrayList<>();
for (Map.Entry<Integer, List<String>> entry : rootToEmails.entrySet()) {
int root = entry.getKey();
List<String> emails = entry.getValue();
Collections.sort(emails);

List<String> 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;
}
if (a.size() == 1 || b.size() == 1) {
return Integer.compare(a.size(), b.size());
}
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]++;
}
}
}
}
61 changes: 61 additions & 0 deletions src/test/java/com/thealgorithms/graph/AccountMergeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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<List<String>> 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<List<String>> merged = AccountMerge.mergeAccounts(accounts);

List<List<String>> 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<List<String>> accounts = List.of(List.of("Alex", "alex1@mail.com"), List.of("Alex", "alex2@mail.com"));

List<List<String>> merged = AccountMerge.mergeAccounts(accounts);
List<List<String>> 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()));
}

@Test
void testNullInput() {
assertEquals(List.of(), AccountMerge.mergeAccounts(null));
}

@Test
void testTransitiveMergeAndDuplicateEmails() {
List<List<String>> 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<List<String>> merged = AccountMerge.mergeAccounts(accounts);

List<List<String>> 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<List<String>> accounts = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob"));

List<List<String>> merged = AccountMerge.mergeAccounts(accounts);
List<List<String>> expected = List.of(List.of("Alex"), List.of("Alex", "alex1@mail.com"), List.of("Bob"));

assertEquals(expected, merged);
}
}
Loading