美文网首页
并查集应用-账户合并

并查集应用-账户合并

作者: 铜炉 | 来源:发表于2021-01-20 21:17 被阅读0次

    前言

    力扣连着出了好几天的并查集题目,应该是想让读者学不会并查集就别过年的意思...所以今天来记录一道并查集的应用,账户合并问题

    题目

    //给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是 名称 (name),其
    //余元素是 emails 表示该账户的邮箱地址。 
    //
    // 现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为
    //人们可能具有相同的名称。一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。 
    //
    // 合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是按顺序排列的邮箱地址。账户本身可以以任意顺序返回。 
    //
    // 
    //
    // 示例 1: 
    //
    // 
    //输入:
    //accounts = [["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnn
    //ybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Ma
    //ry", "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']] 也是正确的
    

    分析

    先拆解一下题目

    题设:有若干账户(数组),账户里包含了用户姓名(数组第一个元素)以及该用户的若干不重复的邮箱
    目的:要将相同用户的账户进行合并(把相同用户的邮箱整合到一个数组中)

    好题目分析完,先直观的对题目进行一些思考

    1、将相同用户的账户合并,优先要找到相同用户。
    2、因为存在重名情况,不可以根据用户名来区分(也就是不能直接用一个hashmap把用户名当key来存储)。
    3、排除掉不能用用户名当key的情况,那么只能根据账户中拥有相同邮箱来标记同人。
    4、最笨的办法,可以遍历每一个邮件,然后在全集中找这个邮箱的账户,把该账户合并,更新账户集,记录当前邮箱已经处理完毕,继续遍历其他邮箱。
    5、显然,这种办法可行,但是时间复杂度太高了O(n2),每个邮箱都要跟其他所有邮箱进行比较判断。要优化
    6、在分析,我们的目的是通过相等的两个邮箱,把两个账户合并到一起。那么我们如果能够建立一个索引关系,通过一个账户的任意一个邮箱就可以快速找到账户,那么遇到相等的两个相等邮箱的时候,我们直接把一个集合纳入到另外一个集合就ok了。
    7、可以把每一个账户看成一颗树,树里的每一个叶子节点都是邮箱,当另外一个账户拥有这课树的元素时,直接合并。那并查集的大概思路就出来了。

    分析完毕,进行解题思路

    1、首先需要遍历所有的邮箱,将每一个邮箱自身作为一个集合。
    2、遍历所有账户,把账户中的邮箱合并到一棵树中。(注1)
    3、把树的节点合并到一个数组。
    4、把数组的第一个元素标记为名字,返回结果。

    注1:
    举个例子,现在有两个账户 accountA、accountB,accountA中包含emailA,emailB。accountB中包含emailA,emailC。遍历accountA的时候,我们将emailA、emailB合并到了一棵树中。在遍历accountB时,将emailA与emailC合并时,因为emailA已经所属于一颗构造好的树,所以实际上是将emailC纳入到这棵树来,完成了合并。

    实现

    并查集

    class UnionFind {
        // 有多少个元素,初始化的时候就数组的长度就是多少,每一个元素初始的父节点都是自身,即根节点
        // 数组下标代表一个叶子节点,下标对应的值代表树的父节点.
        // 数组元素和下标相同的点,代表是根节点,找到根节点,就找到了元素所属的集合.
        int[] parent;
    
        // 初始化并查集,初始化时,每个元素都是根节点,每个元素都是一棵树
        public UnionFind(int n) {
            parent = new int[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;
            }
        }
    
        // 合并两棵树,将一棵树(下标)在数组中的值赋值为另外一棵树的下标值,即标记了所属同一颗树.
        public void union(int index1, int index2) {
            parent[find(index2)] = find(index1);
        }
    
        // 找到下标和数组下标相同的点,这就是根节点.也就是找老大环节.
        public int find(int index) {
            if (parent[index] != index) {
                parent[index] = find(parent[index]);
            }
            return parent[index];
        }
    }
    

    实现逻辑

    class Solution {
        public List<List<String>> accountsMerge(List<List<String>> accounts) {
            // 构建邮箱与并查集初始化时的索引关系
            Map<String, Integer> emailToIndex = new HashMap<String, Integer>();
            // 构建邮箱与用户的映射关系
            Map<String, String> emailToName = new HashMap<String, String>();
            int emailsCount = 0;
            // 遍历所有邮箱,找到一共有多少个邮箱即为有多少颗树
            for (List<String> account : accounts) {
                String name = account.get(0);
                int size = account.size();
                for (int i = 1; i < size; i++) {
                    String email = account.get(i);
                    if (!emailToIndex.containsKey(email)) {
                        // 每找到一颗不同的树,树的 总数+1
                        emailToIndex.put(email, emailsCount++);
                        emailToName.put(email, name);
                    }
                }
            }
            // 根据一共邮箱的数量(即树的数量,构建并查集)
            UnionFind uf = new UnionFind(emailsCount);
            for (List<String> account : accounts) {
                String firstEmail = account.get(1);
                // 找到邮箱在并查集中对应的下标
                int firstIndex = emailToIndex.get(firstEmail);
                int size = account.size();
                for (int i = 2; i < size; i++) {
                    // 逐一获取其他邮箱的下标
                    String nextEmail = account.get(i);
                    int nextIndex = emailToIndex.get(nextEmail);
                    // 因为当前在一个account中,所以需要将两个邮箱合并,即合到一棵树中
                    uf.union(firstIndex, nextIndex);
                }
            }
            // 构建森林中每颗树的根节点坐标与这棵树所有邮箱列表的映射关系
            Map<Integer, List<String>> indexToEmails = new HashMap<Integer, List<String>>();
            for (String email : emailToIndex.keySet()) {
                // 找到邮箱所在的树的根节点
                int index = uf.find(emailToIndex.get(email));
                // 构建邮箱集合,将邮箱添加到集合中
                List<String> account = indexToEmails.getOrDefault(index, new ArrayList<String>());
                account.add(email);
                indexToEmails.put(index, account);
            }
            // 构建结果
            List<List<String>> merged = new ArrayList<List<String>>();
            for (List<String> emails : indexToEmails.values()) {
                Collections.sort(emails);
                // 获取用户名
                String name = emailToName.get(emails.get(0));
                List<String> account = new ArrayList<String>();
                // 第一位是用户名
                account.add(name);
                // 将邮箱补充进数据
                account.addAll(emails);
                merged.add(account);
            }
            return merged;
        }
    
    }
    

    相关文章

      网友评论

          本文标题:并查集应用-账户合并

          本文链接:https://www.haomeiwen.com/subject/ilklzktx.html