← 返回题目列表

账户合并问题如何建图?DFS 和并查集分别怎么做?

高频 中等 第 13 / 30 题 更新于 2026/07/30
并查集DFS哈希表连通分量

简化版

账户合并的本质是连通分量:同一个账户里的邮箱属于同一个人,邮箱之间可以连边。最后把所有互相连通的邮箱归为一组,并按字典序输出。可以用 DFS 建邮箱图,也可以用并查集合并邮箱。

详细版

DFS 做法:把每个账户的第一个邮箱作为中心,和该账户中的其他邮箱连边,同时记录邮箱对应的用户名。建完图后,对邮箱图求连通分量,每个分量排序后加上用户名。

并查集做法:把邮箱作为节点,同一账户内的邮箱 union 到一起;最后按根节点分组。并查集更适合“合并关系”表达,DFS 更容易从图连通性角度理解。复杂度主要由邮箱数量和排序决定。

完整版教学

一、为什么账户合并是图问题

题目说如果两个账户共享至少一个邮箱,就属于同一个人。共享邮箱会把不同账户连接起来,连接关系还具有传递性:A 和 B 共享邮箱,B 和 C 共享另一个邮箱,那么 A、B、C 都要合并。

Account1: John a@mail b@mail
Account2: John b@mail c@mail
Account3: Mary x@mail

a -- b -- c   是 John 的连通分量
x             是 Mary 的连通分量

因此真正的节点应是邮箱,而不是账户行。账户行只是告诉我们这一批邮箱之间应该连通。

二、DFS 建图:中心邮箱连边

一个账户里如果有 k 个邮箱,没必要两两连边形成完全图。把第一个邮箱作为中心,连向其余 k-1 个邮箱,就能保证这一账户内所有邮箱连通,边数更少。

账户邮箱数两两连边中心连边
33 条2 条
1045 条9 条
1004950 条99 条

这个优化非常实用。建图时还要记录 email -> name,因为最终输出每组邮箱前面要放用户名。

三、DFS 代码流程

建好邮箱邻接表后,对每个未访问邮箱做 DFS,收集一个连通分量。收集完排序,再把用户名插到最前面。

Map<String, List<String>> graph = new HashMap<>();
Map<String, String> emailToName = new HashMap<>();
for (List<String> acc : accounts) {
    String name = acc.get(0);
    String first = acc.get(1);
    for (int i = 1; i < acc.size(); i++) {
        String email = acc.get(i);
        emailToName.put(email, name);
        graph.putIfAbsent(email, new ArrayList<>());
        graph.putIfAbsent(first, new ArrayList<>());
        graph.get(first).add(email);
        graph.get(email).add(first);
    }
}

DFS 部分就是标准连通分量遍历。注意账户只有一个邮箱时,也要把这个邮箱放进图,否则它可能被漏掉。

四、并查集做法

并查集把同一账户内的邮箱合并到同一个集合。由于邮箱是字符串,通常用 Map<String, String> parent,或者先把邮箱映射成整数编号。

for account in accounts:
  first = account[1]
  for email in account[1..]:
    union(first, email)

for email in allEmails:
  root = find(email)
  group[root].add(email)

如果有 10000 个邮箱,并查集的合并和查找几乎是常数级。最后排序每个分组,排序成本是不可避免的,因为题目要求邮箱按字典序输出。

五、用户名如何处理

经典题默认同一个人的账户名相同,所以分组后用任意一个邮箱对应的名字即可。更稳妥的做法是在读取账户时记录每个邮箱的名字,输出时取分组根邮箱或分组第一个邮箱对应的名字。

emailToName["a@mail"] = "John"
root(a@mail) = b@mail
group[b@mail] = [a@mail, b@mail, c@mail]
output = ["John", "a@mail", "b@mail", "c@mail"]

如果现实业务里同一个邮箱可能绑定不同名字,就需要额外冲突规则;但算法题通常不考这个业务异常。

六、复杂度分析

设总邮箱出现次数为 S,去重后邮箱数为 E。DFS 建图和遍历是 O(S + E) 量级,排序所有分组总成本最坏 O(E log E)。并查集合并是 O(S α(E)),分组和排序同样需要处理所有邮箱。

方法时间主项空间适合表达
DFS 图建图 + 遍历 + 排序邻接表较大连通分量直观
并查集union/find + 排序parent map合并关系清晰

面试时两种都可以,若题目叫“merge”,并查集往往更自然;若想强调图建模,DFS 也很好讲。

七、常见误区与追问

记忆钩子:账户行不是最终节点,邮箱才是节点;共享邮箱把账户串成连通分量。

  • 误区:按账户名合并。 同名不一定同人,必须按共享邮箱合并。
  • 误区:只合并直接共享的两行账户。 共享关系有传递性,要合并整个连通分量。
  • 误区:同一账户内邮箱两两连边。 可以中心连边,连通性相同但边数少很多。
  • 追问:只有一个邮箱的账户怎么处理? 建图或 parent 初始化时必须加入该邮箱,否则会漏结果。
  • 追问:最终为什么要排序? 题目通常要求每组邮箱按字典序输出,遍历顺序不能保证有序。
  • 追问:DFS 和并查集怎么选? DFS 建模直观,并查集更贴近动态合并;复杂度都主要受邮箱数量和排序影响。

八、加强记忆

账户合并的核心不是账户名,而是邮箱连通性。同一账户内邮箱连通,共享邮箱让多个账户连通,最终每个连通分量就是一个合并后的账户。DFS 做法是建邮箱图再找连通分量;并查集做法是 union 同账户邮箱再按根分组。最后别忘了邮箱去重、单邮箱账户和排序输出。