Skip to content
Reliable Data Engineering
Practice problem medium union-findgraphsentity-resolution
Practise with timer, notes and rubric

Merge Customer Records That Share an Email (Union-Find)

Pattern: Graphs: BFS, DFS and Union-Find · Difficulty: Medium · Asked at: Meta, Google, Amazon, Salesforce

Classic version: LeetCode 721 · LeetCode 547

Problem

Each record is [name, email1, email2, ...]. Two records belong to the same person if they share any email (directly or through a chain of records). Merge them and return one entry per person: [name, *sorted unique emails]. Sort the result by the first email. Records of one person always carry the same name.

Examples

merge_accounts([
    ["John", "john@a.com", "john_work@b.com"],
    ["John", "john@a.com", "john00@c.com"],
    ["Mary", "mary@m.com"],
    ["John", "johnny@d.com"],
]) → [["John", "john00@c.com", "john@a.com", "john_work@b.com"],
      ["John", "johnny@d.com"],
      ["Mary", "mary@m.com"]]

Starter code

def merge_accounts(records: list[list[str]]) -> list[list[str]]:
    pass

Hints

Hint 1

Treat each email as a node; every record connects its emails. People are the connected components.

Hint 2

Union-find: union each email in a record with the record’s first email. Then group emails by their root.

Where this shows up in data engineering

This is identity resolution for a Customer 360: link CRM, web and support records through shared emails, phones or device IDs. In production the matching keys are normalised first (lower-case, strip dots/plus-tags), and the clustering runs as connected components in Spark/GraphFrames or iterative SQL. Also watch for “super-nodes” (a shared support@ email) that would wrongly merge thousands of people.

Solution

def merge_accounts(records):
    parent: dict[str, str] = {}

    def find(x):
        parent.setdefault(x, x)
        while parent[x] != x:
            parent[x] = parent[parent[x]]        # path halving
            x = parent[x]
        return x

    def union(a, b):
        ra, rb = find(a), find(b)
        if ra != rb:
            parent[rb] = ra

    owner = {}
    for name, *emails in records:
        for e in emails:
            owner[e] = name
            union(emails[0], e)

    groups: dict[str, list[str]] = {}
    for e in owner:
        groups.setdefault(find(e), []).append(e)
    merged = [[owner[root], *sorted(es)] for root, es in groups.items()]
    return sorted(merged, key=lambda acc: acc[1])

Tests

Your solution should pass these:

recs = [
    ["John", "john@a.com", "john_work@b.com"],
    ["John", "john@a.com", "john00@c.com"],
    ["Mary", "mary@m.com"],
    ["John", "johnny@d.com"],
]
assert merge_accounts(recs) == [
    ["John", "john00@c.com", "john@a.com", "john_work@b.com"],
    ["John", "johnny@d.com"],
    ["Mary", "mary@m.com"],
]
chain = [["A", "a1", "a2"], ["A", "a3", "a4"], ["A", "a2", "a3"]]
assert merge_accounts(chain) == [["A", "a1", "a2", "a3", "a4"]]
assert merge_accounts([["Z", "z@z.com", "z@z.com"]]) == [["Z", "z@z.com"]]

Explanation

Union-find maintains disjoint sets with near-constant find/union (inverse Ackermann with path compression + union by rank; path halving alone is plenty in practice). Union every email in a record with the record’s first email; afterwards, emails with the same root belong to one person.

Complexity: O(E·α(E)) for unions plus O(E log E) to sort emails, where E is the total number of emails.

BFS/DFS alternative: build an adjacency list email → emails and find components; same asymptotics, more memory for the explicit graph.

Transitive closure is the point: records 1 and 3 in the chain test share nothing directly; they’re linked through record 2. A simple GROUP BY email can’t find that.

Follow-up questions

How would you do this for 500 million customer records?

Normalise keys, then run connected components as a distributed graph job (GraphFrames, or iterative min-label propagation in SQL until no label changes). Guard against super-nodes by excluding identifiers shared by more than N records, and store the resulting cluster_id with match evidence for auditability (see the Customer 360 data modeling case).