721. 账户合并
题目描述
输入列表 accounts。每一项 accounts[i] 是一个字符串列表:accounts[i][0] 是用户名,后面的元素都是这个账户登记的邮箱。
只要两个账户有至少一个相同的邮箱,就认定它们属于同一个人,这种关系可以传递。名字相同但没有共享邮箱的账户,可能是不同的人。同一个人的所有账户名字一定相同。
把属于同一个人的账户合并成一个,返回合并结果。每个账户第一个元素是名字,后面是此人全部邮箱,去重后按 ASCII 字典序升序排列。各账户之间的先后顺序不限。
输入:accounts = [["John","johnsmith@mail.com","john_newyork@mail.com"],["John","johnsmith@mail.com","john00@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]] 输出:[["John","john00@mail.com","john_newyork@mail.com","johnsmith@mail.com"],["Mary","mary@mail.com"],["John","johnnybravo@mail.com"]]
解释:第 1、2 个账户共享 johnsmith@mail.com,合并为一个;另外两个账户各自独立。
输入:accounts = [["Gabe","Gabe0@m.co","Gabe3@m.co","Gabe1@m.co"],["Kevin","Kevin3@m.co","Kevin5@m.co","Kevin0@m.co"],["Ethan","Ethan5@m.co","Ethan4@m.co","Ethan0@m.co"],["Hanzo","Hanzo3@m.co","Hanzo1@m.co","Hanzo0@m.co"],["Fern","Fern5@m.co","Fern1@m.co","Fern0@m.co"]] 输出:[["Gabe","Gabe0@m.co","Gabe1@m.co","Gabe3@m.co"],["Kevin","Kevin0@m.co","Kevin3@m.co","Kevin5@m.co"],["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Hanzo","Hanzo0@m.co","Hanzo1@m.co","Hanzo3@m.co"],["Fern","Fern0@m.co","Fern1@m.co","Fern5@m.co"]]
解释:五个账户互相没有共同邮箱,各自只需把邮箱排序。
数据范围
1 <= accounts.length <= 10002 <= accounts[i].length <= 101 <= accounts[i][j].length <= 30accounts[i][0]只含英文字母。j > 0时accounts[i][j]都是合法邮箱。
解题思路:并查集
把账户编号当作并查集的节点。用哈希表 owner 记录每个邮箱第一次出现在哪个账户。扫描账户 i 的邮箱时,如果邮箱已被账户 j 记录,说明 i 和 j 是同一个人,合并两者的根;否则记录 owner[email] = i。最后按每个邮箱的根把邮箱分组,排序后在前面加上根账户的名字。
为什么正确
扫描过程中,两个账户在同一集合当且仅当它们通过已扫描的共享邮箱(直接或间接)相连。
复杂度分析
时间 O(S log S),S 为邮箱总数,主要是排序;并查集操作近似常数。空间 O(S)。
易错点
不能只按名字合并:同名的人可能不是同一个人。也不能只比较相邻账户,必须用并查集处理传递关系(A 与 B 共享、B 与 C 共享,则 A、C 也要合并)。
参考代码
Python、Java、C++、Go、JavaScript、TypeScript 六种语言,逻辑一致。
class Solution:
def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
parent = list(range(len(accounts)))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
owner = {}
for i, acc in enumerate(accounts):
for email in acc[1:]:
if email in owner:
parent[find(i)] = find(owner[email])
else:
owner[email] = i
groups = collections.defaultdict(set)
for email, i in owner.items():
groups[find(i)].add(email)
return [[accounts[root][0]] + sorted(emails) for root, emails in groups.items()]class Solution {
int[] parent;
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public List<List<String>> accountsMerge(List<List<String>> accounts) {
int n = accounts.size();
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
Map<String, Integer> owner = new HashMap<>();
for (int i = 0; i < n; i++) {
List<String> acc = accounts.get(i);
for (int k = 1; k < acc.size(); k++) {
String email = acc.get(k);
if (owner.containsKey(email)) {
parent[find(i)] = find(owner.get(email));
} else {
owner.put(email, i);
}
}
}
Map<Integer, List<String>> groups = new HashMap<>();
for (Map.Entry<String, Integer> e : owner.entrySet()) {
groups.computeIfAbsent(find(e.getValue()), k -> new ArrayList<>()).add(e.getKey());
}
List<List<String>> res = new ArrayList<>();
for (Map.Entry<Integer, List<String>> e : groups.entrySet()) {
List<String> emails = e.getValue();
Collections.sort(emails);
List<String> row = new ArrayList<>();
row.add(accounts.get(e.getKey()).get(0));
row.addAll(emails);
res.add(row);
}
return res;
}
}class Solution {
public:
vector<int> parent;
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
int n = accounts.size();
parent.resize(n);
iota(parent.begin(), parent.end(), 0);
unordered_map<string, int> owner;
for (int i = 0; i < n; i++) {
for (int k = 1; k < (int)accounts[i].size(); k++) {
const string& email = accounts[i][k];
if (owner.count(email)) {
parent[find(i)] = find(owner[email]);
} else {
owner[email] = i;
}
}
}
map<int, vector<string>> groups;
for (auto& [email, i] : owner) {
groups[find(i)].push_back(email);
}
vector<vector<string>> res;
for (auto& [root, emails] : groups) {
sort(emails.begin(), emails.end());
vector<string> row = {accounts[root][0]};
row.insert(row.end(), emails.begin(), emails.end());
res.push_back(row);
}
return res;
}
};func accountsMerge(accounts [][]string) [][]string {
parent := make([]int, len(accounts))
for i := range parent {
parent[i] = i
}
var find func(x int) int
find = func(x int) int {
for parent[x] != x {
parent[x] = parent[parent[x]]
x = parent[x]
}
return x
}
owner := map[string]int{}
for i, acc := range accounts {
for _, email := range acc[1:] {
if j, ok := owner[email]; ok {
parent[find(i)] = find(j)
} else {
owner[email] = i
}
}
}
groups := map[int][]string{}
for email, i := range owner {
r := find(i)
groups[r] = append(groups[r], email)
}
res := [][]string{}
for root, emails := range groups {
sort.Strings(emails)
res = append(res, append([]string{accounts[root][0]}, emails...))
}
return res
}var accountsMerge = function(accounts) {
const parent = accounts.map((_, i) => i);
const find = (x) => {
while (parent[x] !== x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
};
const owner = new Map();
accounts.forEach((acc, i) => {
for (const email of acc.slice(1)) {
if (owner.has(email)) {
parent[find(i)] = find(owner.get(email));
} else {
owner.set(email, i);
}
}
});
const groups = new Map();
for (const [email, i] of owner) {
const r = find(i);
if (!groups.has(r)) groups.set(r, []);
groups.get(r).push(email);
}
const res = [];
for (const [root, emails] of groups) {
emails.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
res.push([accounts[root][0], ...emails]);
}
return res;
};function accountsMerge(accounts: string[][]): string[][] {
const parent = accounts.map((_, i) => i);
const find = (x: number): number => {
while (parent[x] !== x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
};
const owner = new Map<string, number>();
accounts.forEach((acc, i) => {
for (const email of acc.slice(1)) {
if (owner.has(email)) {
parent[find(i)] = find(owner.get(email)!);
} else {
owner.set(email, i);
}
}
});
const groups = new Map<number, string[]>();
for (const [email, i] of owner) {
const r = find(i);
if (!groups.has(r)) groups.set(r, []);
groups.get(r)!.push(email);
}
const res: string[][] = [];
for (const [root, emails] of groups) {
emails.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
res.push([accounts[root][0], ...emails]);
}
return res;
}其他解法
两种解法都在求邮箱的连通分量:并查集边读边合并,不需要建图;深度优先搜索先建图再遍历,更符合图论直觉。排序是两者共同的主要开销。
解法 2:建图 + 深度优先搜索 时间 O(S log S),空间 O(S)
把每个邮箱看成图中的点。每个账户里,把第一个邮箱和其余邮箱各连一条无向边,并记录邮箱对应的名字。然后遍历所有邮箱,从未访问的邮箱出发用栈做深度优先搜索,收集同一连通分量的全部邮箱。分量内排序后,在前面加上名字。
思路直接,就是求连通分量。需要显式建邻接表,内存比并查集多一些。并查集还支持边读边合并,适合在线场景。
class Solution:
def accountsMerge(self, accounts: List[List[str]]) -> List[List[str]]:
graph = collections.defaultdict(list)
name_of = {}
for acc in accounts:
first = acc[1]
for email in acc[1:]:
name_of[email] = acc[0]
graph[first].append(email)
graph[email].append(first)
seen = set()
res = []
for email in name_of:
if email in seen:
continue
seen.add(email)
stack, comp = [email], []
while stack:
u = stack.pop()
comp.append(u)
for v in graph[u]:
if v not in seen:
seen.add(v)
stack.append(v)
res.append([name_of[email]] + sorted(comp))
return resclass Solution {
public List<List<String>> accountsMerge(List<List<String>> accounts) {
Map<String, List<String>> graph = new HashMap<>();
Map<String, String> nameOf = new HashMap<>();
for (List<String> acc : accounts) {
String first = acc.get(1);
for (int k = 1; k < acc.size(); k++) {
String email = acc.get(k);
nameOf.put(email, acc.get(0));
graph.computeIfAbsent(first, x -> new ArrayList<>()).add(email);
graph.computeIfAbsent(email, x -> new ArrayList<>()).add(first);
}
}
Set<String> seen = new HashSet<>();
List<List<String>> res = new ArrayList<>();
for (String email : nameOf.keySet()) {
if (seen.contains(email)) continue;
seen.add(email);
Deque<String> stack = new ArrayDeque<>();
stack.push(email);
List<String> comp = new ArrayList<>();
while (!stack.isEmpty()) {
String u = stack.pop();
comp.add(u);
for (String v : graph.get(u)) {
if (seen.add(v)) stack.push(v);
}
}
Collections.sort(comp);
List<String> row = new ArrayList<>();
row.add(nameOf.get(email));
row.addAll(comp);
res.add(row);
}
return res;
}
}class Solution {
public:
vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
unordered_map<string, vector<string>> graph;
unordered_map<string, string> nameOf;
for (auto& acc : accounts) {
const string& first = acc[1];
for (int k = 1; k < (int)acc.size(); k++) {
nameOf[acc[k]] = acc[0];
graph[first].push_back(acc[k]);
graph[acc[k]].push_back(first);
}
}
unordered_set<string> seen;
vector<vector<string>> res;
for (auto& [email, name] : nameOf) {
if (seen.count(email)) continue;
seen.insert(email);
vector<string> stack = {email}, comp;
while (!stack.empty()) {
string u = stack.back();
stack.pop_back();
comp.push_back(u);
for (auto& v : graph[u]) {
if (!seen.count(v)) {
seen.insert(v);
stack.push_back(v);
}
}
}
sort(comp.begin(), comp.end());
vector<string> row = {name};
row.insert(row.end(), comp.begin(), comp.end());
res.push_back(row);
}
return res;
}
};func accountsMerge(accounts [][]string) [][]string {
graph := map[string][]string{}
nameOf := map[string]string{}
for _, acc := range accounts {
first := acc[1]
for _, email := range acc[1:] {
nameOf[email] = acc[0]
graph[first] = append(graph[first], email)
graph[email] = append(graph[email], first)
}
}
seen := map[string]bool{}
res := [][]string{}
for email, name := range nameOf {
if seen[email] {
continue
}
seen[email] = true
stack, comp := []string{email}, []string{}
for len(stack) > 0 {
u := stack[len(stack)-1]
stack = stack[:len(stack)-1]
comp = append(comp, u)
for _, v := range graph[u] {
if !seen[v] {
seen[v] = true
stack = append(stack, v)
}
}
}
sort.Strings(comp)
res = append(res, append([]string{name}, comp...))
}
return res
}var accountsMerge = function(accounts) {
const graph = new Map(), nameOf = new Map();
const link = (a, b) => {
if (!graph.has(a)) graph.set(a, []);
graph.get(a).push(b);
};
for (const acc of accounts) {
const first = acc[1];
for (const email of acc.slice(1)) {
nameOf.set(email, acc[0]);
link(first, email);
link(email, first);
}
}
const seen = new Set(), res = [];
for (const [email, name] of nameOf) {
if (seen.has(email)) continue;
seen.add(email);
const stack = [email], comp = [];
while (stack.length) {
const u = stack.pop();
comp.push(u);
for (const v of graph.get(u)) {
if (!seen.has(v)) {
seen.add(v);
stack.push(v);
}
}
}
comp.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
res.push([name, ...comp]);
}
return res;
};function accountsMerge(accounts: string[][]): string[][] {
const graph = new Map<string, string[]>(), nameOf = new Map<string, string>();
const link = (a: string, b: string): void => {
if (!graph.has(a)) graph.set(a, []);
graph.get(a)!.push(b);
};
for (const acc of accounts) {
const first = acc[1];
for (const email of acc.slice(1)) {
nameOf.set(email, acc[0]);
link(first, email);
link(email, first);
}
}
const seen = new Set<string>(), res: string[][] = [];
for (const [email, name] of nameOf) {
if (seen.has(email)) continue;
seen.add(email);
const stack = [email], comp: string[] = [];
while (stack.length) {
const u = stack.pop()!;
comp.push(u);
for (const v of graph.get(u)!) {
if (!seen.has(v)) {
seen.add(v);
stack.push(v);
}
}
}
comp.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
res.push([name, ...comp]);
}
return res;
}