央企都这样了,确实有点绷不住。以前大家总觉得央企稳,像个铁饭碗,结果现在饭碗还在,碗边开始掉瓷了。
第一个选项,辞退,按 N 或者 N+1 给钱,听着最干脆,疼是疼,但至少账算得明白。第二个改劳务派遣,工作照旧,身份换了,年金没了,这就很微妙,活还是你干,保障先抽走一块。第三个更绝,发最低工资,让你自己出去找工作,期限还不说,这种最磨人,钱不多,心还悬着。

最难受的不是选哪个,是这三个你都不想要,但偏偏又得选一个。
很多时候职场的残酷就在这儿,它不一定直接把门关上,它给你留几条路,看着像选择,其实每条都硌脚。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.COM、a@mail.com、a@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,日志全是正常,运营过两天才发现用户资产挂错了。到那时候再补,基本就是导数据、写脚本、对账,一整套体力活。
所以我宁愿一开始代码啰嗦一点,也要把合并依据写清楚。
并查集只是工具,真正要盯住的是那条边。