央企,刚被通知要裁员。给了我3个选项 ,辞退,补偿按N或者N+1~
央企都这样了,确实有点绷不住。以前大家总觉得央企稳,像个铁饭碗,结果现在饭碗还在,碗边开始掉瓷了。
第一个,辞退,按N或者N+1给钱,听着最干脆,疼是疼,但至少账算得明白。第二个改劳务派遣,工作照旧,身份换了,年金没了,这就很微妙了,活还是你干,保障先抽走一块。第三个更绝,发最低工资,让你自己出去找工作,期限还不说,这种最磨人,钱不多,心还悬着。
最难受的不是选哪个,是这三个你都不想要,但偏偏又得选一个。
很多时候职场的残酷就在这儿,它不一定直接把门关上,它给你留几条路,看着像选择,其实每条都硌脚。HR嘴上可能还挺客气,流程也都合规,可人听完就是发懵。
面试题:账户合并
导入完客户账号,后台多出来一堆重复人。
最烦的是,名字还都一样。 运营一看:“张三怎么有三个账号?” 我第一眼就不信名字,名字这东西太脏了,同名、改名、空格、大小写,全都可能坑你。
账户合并真正能信的,一般是邮箱、手机号、证件号这种能当“边”的东西。比如这批数据长这样:
["John", "[email protected]", "[email protected]"]
["John", "[email protected]", "[email protected]"]
["Mary", "[email protected]"]
["John", "[email protected]"]
第一条和第二条有同一个邮箱 [email protected],那它们就应该合成一个账户。
注意,不是因为名字都叫 John。 而是因为它们之间有一条能连上的证据。
这个问题我一般不写成一堆双重循环去比。
有人会这么干:拿第一个账户,去扫后面所有账户,看有没有公共邮箱,有就合并。合并完再扫一遍。数据量小的时候没事,数据一大,CPU 马上开始喘。
这种写法看着直观,实际上很容易写出一坨补丁:
A 和 B 有交集,合并
B 和 C 有交集,合并
结果 A 和 C 也得合并
你只要漏处理一次“间接关系”,结果就脏。
我更愿意把它当成连通块问题。
邮箱是点。 同一个账户里的邮箱,默认属于同一个人。 只要两个邮箱能通过某些账户串起来,就归到同一个集合里。
这时候并查集就挺合适。
代码我会这么写,尽量别搞太多花活,排查起来也方便。
import java.util.*;
publicclassAccountMergeJob{
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();
}
privatestaticclassUnionSet{
privatefinal List<Integer> parent = new ArrayList<>();
privatefinal List<Integer> size = new ArrayList<>();
intidOf(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;
}
intfind(int x){
int p = parent.get(x);
if (p != x) {
parent.set(x, find(p));
}
return parent.get(x);
}
voidjoin(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));
}
}
}
这里有几个地方别顺手写错。
第一个,邮箱要先清洗。
线上数据不会像算法题那么干净。[email protected]、[email protected] 、 [email protected],这三个在用户眼里可能是一个邮箱,在程序里可不是。至少 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,日志全是正常,运营过两天才发现用户资产挂错了。到那时候再补,基本就是导数据、写脚本、对账,一整套体力活。
所以我宁愿一开始代码啰嗦一点,也要把合并依据写清楚。
并查集只是工具,真正要盯住的是那条边。