721.账户合并

目标

给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其余元素是 emails 表示该账户的邮箱地址。

现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。

合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是 按字符 ASCII 顺序排列 的邮箱地址。账户本身可以以 任意顺序 返回。

示例 1:

输入:accounts = [["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]]
输出:[["John", 'john00@mail.com', 'john_newyork@mail.com', 'johnsmith@mail.com'],  ["John", "johnnybravo@mail.com"], ["Mary", "mary@mail.com"]]
解释:
第一个和第三个 John 是同一个人,因为他们有共同的邮箱地址 "johnsmith@mail.com"。 
第二个 John 和 Mary 是不同的人,因为他们的邮箱地址没有被其他帐户使用。
可以以任何顺序返回这些列表,例如答案 [['Mary','mary@mail.com'],['John','johnnybravo@mail.com'],
['John','john00@mail.com','john_newyork@mail.com','johnsmith@mail.com']] 也是正确的。

示例 2:

输入: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"]]
输出:[["Ethan","Ethan0@m.co","Ethan4@m.co","Ethan5@m.co"],["Gabe","Gabe0@m.co","Gabe1@m.co","Gabe3@m.co"],["Hanzo","Hanzo0@m.co","Hanzo1@m.co","Hanzo3@m.co"],["Kevin","Kevin0@m.co","Kevin3@m.co","Kevin5@m.co"],["Fern","Fern0@m.co","Fern1@m.co","Fern5@m.co"]]

说明:

  • 1 <= accounts.length <= 1000
  • 2 <= accounts[i].length <= 10
  • 1 <= accounts[i][j].length <= 30
  • accounts[i][0] 由英文字母组成
  • accounts[i][j] (for j > 0) 是有效的邮箱地址

思路

现有一个账号名称与邮箱列表组成的二维数组,如果两个账号对应的邮箱有重合,那么认为这两个账号属于同一个人,名称一定相同。但是名称相同不代表账户相同。现在需要将同一个人的账号合并,返回格式为,[名称,邮箱1,邮箱2,...],其中邮箱按 ASCII 排序。注意,同一个记录的邮箱列表中也可能存在相同邮箱,比如 ["Kevin","Kevin4@m.co","Kevin2@m.co","Kevin2@m.co"]

直接的想法是比较名称相同的账户邮箱是否有重合,如果有则合并。先将数据整理一下,换为 Map<name, List<List<Integer>>>,然后判断集合是否有共同元素,有则合并,没有则保留。那么使用什么方式处理集合呢?如果两两比较,时间复杂度为 O(n^2),好在一个账户邮箱最多 9 个,账户数量最多1000个,数据量不大。

如果两个集合有公共邮箱,那么可以使用 a.removeAll(b) 这个函数,它的返回值是布尔类型,如果a集合调用函数之后发生变化,即移除了a与b的公共元素,则返回 true,否则 false。因此当返回 true 时,直接与b合并,否则放回队列。需要注意的问题是,集合列表 {a,b} {c,d} {d,e} {e,f} {f,b} 将 第一个集合 {a,b} 与后面的集合依次两两比较时,直到最后一个才合并为{a,b,f},错过了与前面集合的合并,因此我们需要重新与前面的集合比较。

由于需要反复地比较这些集合,又要将属于同一账户的邮箱集合从集合列表中删除,涉及到集合的动态添加与删除。如果使用 ArrayList,尽管可以使用迭代器来动态添加与删除元素,但是从中间删除效率不高,需要移动数组元素。因此我们选择队列来保存这些集合,由于我们的操作主要在首尾两端,可以使用 ArrayDequeArrayDeque 双端操作效率比 LinkedList 更高,尽管它们都能在 O(1) 时间内完成操作,但是 LinkedList 需要额外的指针操作以及潜在的缓存不命中(不是连续分配的)问题,而 ArrayDeque 基于循环数组实现,只需调整头尾指针即可。

官网题解使用的是并查集,其实刚开始我也想到了使用并查集,但之前都是在图问题中用的,如果两个节点有边连接直接合并,但本题如何判断能否合并或者说是否连通呢?通常我们使用数组列表建图,但这里节点数据的类型不同,考虑使用map,key为邮箱,value为账户下标列表。遍历原二维数组,记录已合并的下标,如果邮箱对应有其它账户下标则进入dfs。

// todo 并查集

代码


/**
 * @date 2024-07-15 8:40
 */
public class AccountsMerge721 {
    public List<List<String>> accountsMerge_v1(List<List<String>> accounts) {
        int n = accounts.size();
        Map<String, List<Integer>> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = 1; j < accounts.get(i).size(); j++) {
                map.computeIfAbsent(accounts.get(i).get(j), x -> new ArrayList<>()).add(i);
            }
        }

        boolean[] visited = new boolean[n];
        List<List<String>> res = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (visited[i]) {
                continue;
            }
            visited[i] = true;
            List<String> account = accounts.get(i);
            int size = account.size();
            Set<String> mailList = new HashSet<>(account.subList(1, size));
            for (int j = 1; j < size; j++) {
                String mail = accounts.get(i).get(j);
                for (Integer index : map.get(mail)) {
                    if (visited[index]) {
                        continue;
                    }
                    dfs(index, accounts, map, mailList, visited);
                }
            }
            ArrayList<String> item = new ArrayList<>(mailList);
            Collections.sort(item);
            item.add(0, accounts.get(i).get(0));
            res.add(item);
        }
        return res;
    }

    public void dfs(int index, List<List<String>> accounts, Map<String, List<Integer>> map, Set<String> mailList, boolean[] visited) {
        visited[index] = true;
        List<String> account = accounts.get(index);
        int size = account.size();
        mailList.addAll(account.subList(1, size));
        for (int j = 1; j < size; j++) {
            String mail = accounts.get(index).get(j);
            for (Integer next : map.get(mail)) {
                if (visited[next]) {
                    continue;
                }
                dfs(next, accounts, map, mailList, visited);
            }
        }
    }

    public List<List<String>> accountsMerge(List<List<String>> accounts) {
        Map<String, Queue<Set<String>>> map = new HashMap<>();
        for (List<String> account : accounts) {
            String name = account.get(0);
            map.putIfAbsent(name, new ArrayDeque<>());
            Queue<Set<String>> queue = map.get(name);
            Set<String> mails = new TreeSet<>();
            for (int i = 1; i < account.size(); i++) {
                mails.add(account.get(i));
            }
            queue.offer(mails);
        }
        List<List<String>> res = new ArrayList<>();
        for (Map.Entry<String, Queue<Set<String>>> entry : map.entrySet()) {
            Queue<Set<String>> queue = entry.getValue();
            List<Set<String>> merged = new ArrayList<>();
            while (!queue.isEmpty()) {
                Set<String> mails = queue.poll();
                int size = queue.size();
                int cnt = 0;
                for (int i = 0; i < size; i++) {
                    Set<String> m = queue.poll();
                    if (m.removeAll(mails)) {
                        // 存在问题,(a,b)(c,d)(d,e)(e,f)(f,b) 最后一个才合并(a,b,f),错过了与前面集合的合并
                        mails.addAll(m);
                        // 这里扩展了执行次数,与前面比较过的元素重新比较
                        size += cnt;
                        cnt = 0;
                    } else {
                        queue.add(m);
                        cnt++;
                    }
                }
                merged.add(mails);
            }

            for (Set<String> set : merged) {
                List<String> l = new ArrayList<>();
                l.add(entry.getKey());
                l.addAll(set);
                res.add(l);
            }
        }

        return res;
    }
}

性能

使用队列

使用dfs

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注