账户合并 图解题解
这道题到底在问什么
- 输入
- John[johnsmith, john_newyork] / John[johnsmith, john00] / Mary[mary] / John[johnnybravo]
- 输出
- [John, john00, john_newyork, johnsmith] / [Mary, mary] / [John, johnnybravo]
最优解:一步一步想明白
- 3记住「邮箱当节点 → 同账户内 union → 按根分组 + 排序 + 冠名」,下面逐帧套它。共享邮箱就是把两个账户连起来的那座桥。
- 4先把所有出现过的邮箱各放一个节点:smith、nyc(john_newyork)、j00(john00)、mary、bravo。此刻它们彼此独立,集合数 = 5,相当于先假设有 5 个互不相干的「人」。
- 5并查集准备就绪:每个邮箱的 parent 先指向自己(自成一集)。接下来按账户顺序扫,遇到同一账户里的邮箱就把它们并起来。
- 6看账户 0(John):它有 smith 和 nyc 两个邮箱。规则是以第一个邮箱 smith 为基准,把后面的邮箱都并到它这一组。
- 7union(smith, nyc):把 smith 的根指到 nyc,两个邮箱连成一条边、归入同一集合(同色)。集合数从 5 降到 4。这俩邮箱确定属于同一个 John。
- 8看账户 1(John):它有 smith 和 j00。注意 smith(标红)之前在账户 0 里出现过,这就是连接两个账户的共享邮箱,说明账户 0 和账户 1 是同一个人!
- 9先对基准邮箱 smith 做 find:smith 的 parent 指向 nyc,顺着走,这一组当前的根是 nyc。找根时顺手做路径压缩,让节点以后更快找到根。
- 10union(smith, j00):把 smith 这一组的根接到 j00 上。于是 smith、nyc、j00 三个邮箱并成同一集合(同色),集合数从 4 降到 3。三个邮箱都归这一个 John。
- 11看账户 2(Mary):只有 mary 一个邮箱。基准就是它自己,union(mary, mary) 等于没动,mary 单独成一集。集合数保持 3。
- 12看账户 3(John):只有 bravo(johnnybravo)一个邮箱。它虽然也叫 John,但和前面那个 John 没有任何共享邮箱。
- 13所以 bravo 不会和前面的 John 合并,它自成一集,这正是本题的坑:同名不代表同一人,要靠共享邮箱判断。四个账户扫完,集合数定格在 3,也就是 3 个不同的人。
- 14四个账户都处理完,看整张图:smith、nyc、j00 三个邮箱连成同一组(一种颜色),mary 自成一色,bravo 自成一色,正好对应面板上的集合数 3。下一步把每一组的邮箱分别收集出来。
- 15分组的办法很直接:对每个邮箱做一次 find 拿到它的根,根相同的就归到同一组里。下面逐个邮箱看。
- 16账户都扫完了,开始分组。对每个邮箱做 find 拿到它的根,根相同的归一组。find(smith) 顺着 smith → … → j00,根是 j00,smith 归到 j00 这组。
- 17find(nyc) 的根也是 j00,nyc 归到同一组。注意路径压缩后,nyc 这些节点会直接挂到根上,下次 find 一步到位。
- 18find(j00) 的根就是 j00 自己。至此 smith、nyc、j00 三个邮箱都归到根 j00 这一组,它们属于同一个 John。
- 19find(mary) 的根是 mary,自己单独一组,属于 Mary。
- 20find(bravo) 的根是 bravo,也单独一组,属于另一个 John。三组分完。
- 21组内排序 + 冠名。j00 这组的邮箱按字典序排成 john00、john_newyork、johnsmith,最前面用 owner 记下的用户名冠上 John → [John, john00, john_newyork, johnsmith]。
- 22Mary 那组只有一个邮箱,排序后就是 [Mary, mary]。
- 23bravo 那组也只有一个,冠上 John → [John, johnnybravo]。和前一个 John 各是各的人。
- 24合并完成:三个人 [John, john00, john_newyork, johnsmith]、[Mary, mary]、[John, johnnybravo]。回头看,我们靠并查集把「共享邮箱 = 同一人」这层关系一边扫一边并起来,集合数 5 → 3 就是答案的人数,既直观又好写。
⚠️ 容易写错的地方
✗ 错:看用户名判断是否同一人
✓ 对:只看有没有共享邮箱
同名账户可能是不同人(如本例两个 John),名字只用来最后冠名,不参与合并判断
✗ 错:union 账户的下标
✓ 对:union 的是邮箱
连通关系建立在邮箱上,共享邮箱才是把两账户连起来的桥;owner 单独记邮箱归谁
✗ 错:忘了排序或冠名
✓ 对:每组邮箱按字典序排、最前面加 owner[根]
题目要求邮箱字典序、且首位是用户名,漏一步答案格式就错
完整代码(Python / C++ / Java)
Python
from typing import List
from collections import defaultdict
class Solution:
def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
parent = {}
owner = {}
def find(x):
parent.setdefault(x, x)
if parent[x] != x:
parent[x] = find(parent[x])
return parent[x]
def union(a, b):
parent[find(a)] = find(b)
for acc in accounts:
name = acc[0]
first = acc[1]
for email in acc[1:]:
owner[email] = name
union(first, email)
groups = defaultdict(list)
for email in owner:
groups[find(email)].append(email)
return [[owner[root]] + sorted(emails) for root, emails in groups.items()]C++
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
using namespace std;
class Solution {
unordered_map<string,string> parent;
string find(const string& x) {
if (!parent.count(x)) parent[x] = x;
if (parent[x] != x) parent[x] = find(parent[x]);
return parent[x];
}
public:
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
parent.clear();
unordered_map<string,string> owner;
for (auto &acc : accounts) {
string first = acc[1];
for (int i = 1; i < (int)acc.size(); ++i) {
owner[acc[i]] = acc[0];
parent[find(first)] = find(acc[i]);
}
}
unordered_map<string, vector<string>> groups;
for (auto &[email, name] : owner) groups[find(email)].push_back(email);
vector<vector<string>> ans;
for (auto &[root, emails] : groups) {
sort(emails.begin(), emails.end());
vector<string> row{owner[root]};
row.insert(row.end(), emails.begin(), emails.end());
ans.push_back(row);
}
return ans;
}
};Java
import java.util.*;
class Solution {
Map<String, String> parent = new HashMap<>();
public List<List<String>> accountsMerge(List<List<String>> accounts) {
parent.clear();
Map<String, String> owner = new HashMap<>();
for (List<String> acc : accounts) {
String first = acc.get(1);
for (int i = 1; i < acc.size(); i++) {
owner.put(acc.get(i), acc.get(0));
parent.put(find(first), find(acc.get(i)));
}
}
Map<String, List<String>> groups = new HashMap<>();
for (String email : owner.keySet()) groups.computeIfAbsent(find(email), k -> new ArrayList<>()).add(email);
List<List<String>> ans = new ArrayList<>();
for (Map.Entry<String, List<String>> e : groups.entrySet()) {
Collections.sort(e.getValue());
ArrayList<String> row = new ArrayList<>();
row.add(owner.get(e.getKey()));
row.addAll(e.getValue());
ans.add(row);
}
return ans;
}
private String find(String x) {
parent.putIfAbsent(x, x);
if (!parent.get(x).equals(x)) parent.put(x, find(parent.get(x)));
return parent.get(x);
}
}复杂度
时间
O(E log E)
E 是邮箱总数。并查集的 union/find 近似 O(1)(反阿克曼);最后每组排序合计 O(E log E),由排序主导
空间
O(E)
parent、owner、分组表都按邮箱数 E 存,O(E)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 账户合并 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不用并查集,改用 DFS/BFS?+
可以。把邮箱(或账户)建成图:同一账户内的邮箱两两连边,然后对图求连通分量,每个连通分量就是一个人。并查集的好处是「边读边合并」、不用显式建图、代码更短;DFS/BFS 思路一样,只是要先建邻接表再遍历。
邮箱是字符串,怎么进并查集?+
直接拿邮箱字符串当哈希表的 key 即可(parent 是 string→string 的 map)。也可以先给每个不同邮箱编一个整数 id、再用数组版并查集,适合追求常数更优的场景;两种都行。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 账户合并 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。