找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3616

积分

0

好友

480

主题
发表于 1 小时前 | 查看: 3| 回复: 0

央企都这样了,确实有点绷不住。以前大家总觉得央企稳,像个铁饭碗,结果现在饭碗还在,碗边开始掉瓷了。

第一个选项,辞退,按 N 或者 N+1 给钱,听着最干脆,疼是疼,但至少账算得明白。第二个改劳务派遣,工作照旧,身份换了,年金没了,这就很微妙,活还是你干,保障先抽走一块。第三个更绝,发最低工资,让你自己出去找工作,期限还不说,这种最磨人,钱不多,心还悬着。

央企裁员求助帖截图,列出辞退N、劳务派遣、发最低工资三个选项

最难受的不是选哪个,是这三个你都不想要,但偏偏又得选一个。

很多时候职场的残酷就在这儿,它不一定直接把门关上,它给你留几条路,看着像选择,其实每条都硌脚。HR 嘴上可能还挺客气,流程也都合规,可人听完就是发懵。

面试题:账户合并

导入完客户,后台多出来一堆重复账号。

最烦的是,名字还都一样。运营一看:“张三怎么有三个账号?”我第一眼就不信名字,名字这东西太脏了,同名、改名、空格、大小写,全都可能坑你。

账户合并真正能信的,一般是邮箱、手机号、证件号这种能当“边”的东西。比如这批数据长这样:

["John", "john_a@mail.com", "john_b@mail.com"]
["John", "john_b@mail.com", "john_c@mail.com"]
["Mary", "mary@mail.com"]
["John", "john_x@mail.com"]

第一条和第二条有同一个邮箱 john_b@mail.com,那它们就应该合成一个账户。

注意,不是因为名字都叫 John。而是因为它们之间有一条能连上的证据。

这个问题我一般不写成一堆双重循环去比。

有人会这么干:拿第一个账户,去扫后面所有账户,看有没有公共邮箱,有就合并。合并完再扫一遍。数据量小的时候没事,数据一大,CPU 马上开始喘。

这种写法看着直观,实际上很容易写出一坨补丁:

A 和 B 有交集,合并
B 和 C 有交集,合并
结果 A 和 C 也得合并

你只要漏处理一次“间接关系”,结果就脏。

我更愿意把它当成连通块问题。

邮箱是点。同一个账户里的邮箱,默认属于同一个人。只要两个邮箱能通过某些账户串起来,就归到同一个集合里。

这时候并查集就挺合适。

代码我会这么写,尽量别搞太多花活,排查起来也方便。

import java.util.*;

public class AccountMergeJob {

    public List<List<String>> mergeAccounts(List<List<String>> rawAccounts) {
        Map<String, Integer> mailIndex = new HashMap<>();
        Map<String, String> mailName = new HashMap<>();
        UnionSet unionSet = new UnionSet();

        for (List<String> row : rawAccounts) {
            if (row == null || row.size() < 2) {
                continue;
            }

            String accountName = cleanName(row.get(0));
            String firstMail = cleanMail(row.get(1));

            int firstId = unionSet.idOf(firstMail, mailIndex);

            mailName.putIfAbsent(firstMail, accountName);

            for (int i = 2; i < row.size(); i++) {
                String mail = cleanMail(row.get(i));
                if (mail.isEmpty()) {
                    continue;
                }

                int mailId = unionSet.idOf(mail, mailIndex);
                unionSet.join(firstId, mailId);
                mailName.putIfAbsent(mail, accountName);
            }
        }

        Map<Integer, TreeSet<String>> grouped = new HashMap<>();

        for (String mail : mailIndex.keySet()) {
            int root = unionSet.find(mailIndex.get(mail));
            grouped.computeIfAbsent(root, k -> new TreeSet<>()).add(mail);
        }

        List<List<String>> result = new ArrayList<>();

        for (TreeSet<String> mails : grouped.values()) {
            String firstMail = mails.first();
            String name = mailName.getOrDefault(firstMail, "UNKNOWN");

            List<String> merged = new ArrayList<>();
            merged.add(name);
            merged.addAll(mails);
            result.add(merged);
        }

        return result;
    }

    private String cleanMail(String mail) {
        if (mail == null) {
            return "";
        }
        return mail.trim().toLowerCase(Locale.ROOT);
    }

    private String cleanName(String name) {
        if (name == null || name.trim().isEmpty()) {
            return "UNKNOWN";
        }
        return name.trim();
    }

    private static class UnionSet {
        private final List<Integer> parent = new ArrayList<>();
        private final List<Integer> size = new ArrayList<>();

        int idOf(String mail, Map<String, Integer> mailIndex) {
            Integer oldId = mailIndex.get(mail);
            if (oldId != null) {
                return oldId;
            }

            int newId = parent.size();
            mailIndex.put(mail, newId);
            parent.add(newId);
            size.add(1);
            return newId;
        }

        int find(int x) {
            int p = parent.get(x);
            if (p != x) {
                parent.set(x, find(p));
            }
            return parent.get(x);
        }

        void join(int a, int b) {
            int rootA = find(a);
            int rootB = find(b);

            if (rootA == rootB) {
                return;
            }

            if (size.get(rootA) < size.get(rootB)) {
                int tmp = rootA;
                rootA = rootB;
                rootB = tmp;
            }

            parent.set(rootB, rootA);
            size.set(rootA, size.get(rootA) + size.get(rootB));
        }
    }
}

这里有几个地方别顺手写错。

第一个,邮箱要先清洗。

线上数据不会像算法题那么干净。A@MAIL.COMa@mail.coma@mail.com,这三个在用户眼里可能是一个邮箱,在程序里可不是。至少 trim 和小写转换要做。

第二个,账户名别拿来做合并依据。

我见过有人直接用 name + email 做 key,结果同一个邮箱因为名字变了,硬是拆成两个账号。还有一种更坑,直接按名字合并,两个同名用户被揉成一个人,后面补数据比写功能还痛苦。

第三个,合并后的名字怎么取,要看业务。

算法题里一般取原账户名就行。业务里不一定。可能要取实名状态最高的那个,可能要取最近登录的那个,也可能要保留主账号昵称。这个规则别藏在合并逻辑里,最好单独做。

比如真实一点的写法,我会把名字选择拆出来:

private String chooseDisplayName(List<String> candidateNames) {
    for (String name : candidateNames) {
        if (name != null && name.startsWith("实名_")) {
            return name.substring(3);
        }
    }

    for (String name : candidateNames) {
        if (name != null && !name.trim().isEmpty()) {
            return name.trim();
        }
    }

    return "UNKNOWN";
}

这段代码不高级,但它把业务味道露出来了。

并查集只管关系,不管展示名。展示名是业务规则,别让它污染关系判断。

再说一个容易忽略的点:合并结果排序。

上面的代码用了 TreeSet,邮箱天然有序。这样做不是为了好看,是为了后面排查方便。

比如测试环境出了问题,你打印日志:

System.out.println(mergedAccount);

每次邮箱顺序都一样,diff 才看得出来。要是用普通 HashSet,每次顺序飘来飘去,排查时人会烦。

如果数据量特别大,TreeSet 的排序成本也要考虑。可以先用 ArrayList 收集,最后统一 Collections.sort()。别上来就优化,先看数据规模。

这类题最关键的不是会不会并查集,而是别把“看起来一样”和“确实有关联”混在一起。

名字一样,不代表同一个人。邮箱连上,才算有证据。手机号、证件号也是一样,都是边。

账户合并做错了,麻烦不是报错,而是静悄悄地产生脏数据。

这种问题最恶心,接口返回 200,日志全是正常,运营过两天才发现用户资产挂错了。到那时候再补,基本就是导数据、写脚本、对账,一整套体力活。

所以我宁愿一开始代码啰嗦一点,也要把合并依据写清楚。

并查集只是工具,真正要盯住的是那条边。




上一篇:DeepSeek V4 Pro API 永久2.5折,比GPT便宜34倍
下一篇:ChatTutor开源AI白板:数学画布+思维导图,让理科讲解不再只靠文字
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-24 21:34 , Processed in 0.611146 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表