LeetCode #721 Medium

Accounts Merge

Accounts Merge is LeetCode 721 (Medium). You get a list accounts, where each entry is a name followed by that account's emails. Merge the entries that belong to the same person.

  • Two accounts belong to the same person if they share at least one email, and that link chains: A with B and B with C puts all three together.
  • A name alone proves nothing. Two different people may both be called John.
  • Each merged account is the name followed by all of its emails in sorted order, and the groups may be returned in any order.

There are up to 1000 accounts of at most 10 emails each, so the work is dominated by sorting the merged groups rather than by the merging itself.

Constraints
  • 1 <= accounts.length <= 1000
  • 2 <= accounts[i].length <= 10
  • 1 <= accounts[i][j].length <= 30
  • accounts[i][0] consists of English letters.
  • accounts[i][j] (for j > 0) is a valid email.
graphunion-findhash-table
Open on LeetCode ↗
02

Intuition

Forget the accounts and look at the emails. Two emails that appear in the same account belong to the same person, and that link spreads: if a and b share an account and b and c share another, all three are one person even though a and c never met.

That is exactly connected components, and union find accounts merge is the standard answer: union-find keyed on the emails builds them in one pass. Union every email in an account with that account's first email, and the components fall out no matter what order the accounts arrive in. The name rides along in a side map, because it identifies nothing.

How to spot this pattern

Whenever things have to be grouped by a "shares something with" rule that chains through intermediaries, you are looking at connected components, and union-find is the direct tool. The move that makes it easy is picking the right node: union the shared token itself, here the email, rather than the records that contain it.

03

Approach

Try it first

Before reading on, take three accounts where the first and third share no email but both share one with the second. Work out by hand why they still belong together, and what you would key a union-find on to get that for free. Aim for O(N log N) time.

1

Union every email with the account's first

Walk the accounts, and for each one union all of its emails with its first email. That links the whole account into one component in a single pass, and because union-find merges by root, two accounts sharing any email end up in the same component without you having to find the overlap yourself.

2

Key the structure on emails, never names

The nodes are the email strings. A name is not an identity: two different people called John must stay apart, and they will, because nothing ever unions their emails. Using names as keys merges strangers together.

3

Remember each email's owner on the side

While scanning, store owner[email] = name in a plain map. Every email belongs to exactly one person, so whichever email you later pick from a finished group hands back the right name to print.

4

Group the emails by their final root

Once all the unions are done, call find on every email and bucket it under the root that comes back. Doing this only at the end matters: a root found mid-scan may still be merged into another later, so the grouping has to wait.

5

Sort each group and put the name in front

The required row is the name followed by the emails in alphabetical order:

  • sort inside each group, not across everything.
  • keep the name out of the sort – it is a label on the row, not one of the sorted values.
04

Accounts Merge solution in Python | C++ | Java

▶1class Solution:
▶2 def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
▶3 parent = {}
▶4 owner = {}
▶5 
▶6 def find(x):
▶7 parent.setdefault(x, x)
▶8 while parent[x] != x:
▶9 parent[x] = parent[parent[x]]
▶10 x = parent[x]
▶11 return x
▶12 
▶13 def union(a, b):
▶14 ra, rb = find(a), find(b)
▶15 if ra != rb:
▶16 parent[ra] = rb
▶17 
▶18 for account in accounts:
▶19 name, emails = account[0], account[1:]
▶20 for email in emails:
▶21 owner[email] = name
▶22 union(emails[0], email)
▶23 
▶24 groups = {}
▶25 for email in owner:
▶26 groups.setdefault(find(email), []).append(email)
▶27 
▶28 return [
▶29 [owner[emails[0]]] + sorted(emails) for emails in groups.values()
▶30 ]
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailemptygroupsnot yetnothing merged yet
accounts4to scan
parentemptyno emails seen
Idea. The emails are the real identities, not the names. Two emails in the same account belong to one person, and that link chains through shared emails, so the answer is the connected components of the emails. Union-find builds those in one pass, whatever order the accounts arrive in.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohnsmithgroupsnot yetaccount 0: anchor johnsmith
account0John
emailjohnsmithfirst time seen
rootjohnsmithunchanged
Account 0 belongs to John. Its first email johnsmith becomes the anchor that the rest of this account's emails are unioned with, and the name is recorded against the email in a side map.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohn_nyjohn_nyjohn_nygroupsnot yetmerge john_ny with johnsmith
account0John
emailjohn_nyfirst time seen
rootjohn_nyjust merged
Union john_ny with the anchor johnsmith. This email is new, so the two components simply join.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohn_nyjohn_nyjohn_nygroupsnot yetaccount 1: anchor johnsmith
account1John
emailjohnsmithseen before
rootjohn_nyunchanged
Account 1 belongs to John. Its first email johnsmith becomes the anchor that the rest of this account's emails are unioned with, and the name is recorded against the email in a side map.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohn00john_nyjohn00john00john00groupsnot yetmerge john00 with johnsmith
account1John
emailjohn00first time seen
rootjohn00just merged
Union john00 with the anchor johnsmith. This email is new, so the two components simply join.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohn00john_nyjohn00john00john00marymarygroupsnot yetaccount 2: anchor mary
account2Mary
emailmaryfirst time seen
rootmaryunchanged
Account 2 belongs to Mary. Its first email mary becomes the anchor that the rest of this account's emails are unioned with, and the name is recorded against the email in a side map.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohn00john_nyjohn00john00john00marymarybravobravogroupsnot yetaccount 3: anchor bravo
account3John
emailbravofirst time seen
rootbravounchanged
Account 3 belongs to John. Its first email bravo becomes the anchor that the rest of this account's emails are unioned with, and the name is recorded against the email in a side map.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohn00john_nyjohn00john00john00marymarybravobravogroupsJohnjohnsmithjohn_nyjohn00MarymaryJohnbravo3 components
groups3distinct roots
emails5bucketed
All unions done. Now every email is sent through find once and bucketed under the root it lands on. The grouping has to wait until the end, because a root found earlier may itself have been merged into another since.
accounts0Johnjohnsmithjohn_ny1Johnjohnsmithjohn002Marymary3Johnbravoroot of each emailjohnsmithjohn00john_nyjohn00john00john00marymarybravobravogroupsJohnjohn00john_nyjohnsmithMarymaryJohnbravosort each group, name in front
answer3 accountsname then sorted emails
Return the rows. Each group is its owner's name followed by its emails in alphabetical order. The name is a label on the row rather than one of the values, so it stays in front and takes no part in the sort.
05

Common pitfalls

Uniting by name

✗ Wrong
union(name, e)
✓ Right
union(emails[0], e)

Different people share names — the problem explicitly warns of this. Only a shared email proves two accounts belong to the same person, so emails must be the union-find keys.

Grouping by the account's index

✗ Wrong
groups.setdefault(find(i), []).append(...)
✓ Right
groups.setdefault(find(e), []).append(e)

Two accounts merge only through their emails, so the component root is an email. Keying groups by account index produces separate entries for accounts that should have merged.

Not sorting the emails

✗ Wrong
result.append([owner[emails[0]]] + emails)
✓ Right
result.append([owner[emails[0]]] + sorted(emails))

The expected output requires each account's emails in sorted order after the name. Union-find yields them in arbitrary traversal order, so the sort is a required final step.

06

Edge cases

Two different people with the same name

Nothing ever unions their emails, so they stay in separate groups and both rows carry that name.

An account whose emails appear nowhere else

Its emails form their own component and come back as a single group, unchanged apart from the sort.

Three accounts chained through a shared email

A merges with B and B with C, so all three land under one root even though A and C share nothing directly.

07

Complexity

Time
O(N log N)
Space
O(N)
The accounts merge python code runs union-find over every email. N is the total number of emails across all accounts. The unions and finds are near constant with path compression, so the cost is dominated by sorting each merged group, which comes to O(N log N) overall. The parent and owner maps hold one entry per email.