今天刷到力扣721的账户合并题,被Erlang整的舒服的不行,把Erlang实现的并查集算法贴一下
题目
原题在[LeetCode721.账户合并](721. 账户合并 - 力扣(LeetCode) (leetcode-cn.com))
给定一个列表 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']] 也是正确的。
简单分析一下,现在有很多个账户,每个账户由一个数组构成,且账户第一个元素为账户名,后面为邮箱名,账户名可以重复,但邮箱名不能重复(一旦重复则视为同一个人),我们目的就是把这些相同账户的人合并起来,且题目告诉我们相同邮箱的人的名字一定相同
思路1
我们可以把整个表遍历,开一个空间来存储这些数据,存储的规则是将名字作为键,所有邮箱作为值,如果相同账户不同邮箱则为两个值
如:[["John", "johnsmith@mail.com", "john00@mail.com"], ["John", "johnnybravo@mail.com"], ["John", "johnsmith@mail.com", "john_newyork@mail.com"], ["Mary", "mary@mail.com"]]
前三项我们都放在John为键的数对中,如下
[{"John", [["johnsmith@mail.com", "john00@mail.com"],["johnnybravo@mail.com"],["johnsmith@mail.com", "john_newyork@mail.com"]]}]
这只是第一步,且在每一步中我们把匹配到的待插入项去遍历对应键的值,看看有没有相同的,如果有则直接合并,合并完作为一个新项插入,(肉眼可见的复杂度,试试看嘛,慢慢优化)
所以就有了以下算法
对应代码
accounts_merge(Accounts) ->
All = do(Accounts, []),
do_print(All, []).
%% 导出答案格式化的方法,将键和值(名字和邮箱拼在一起打印)
do_print([], R) -> R;
do_print([{H, V} | T], R) ->
case length(V) of
1 ->
[VV] = V,
do_print(T, R ++ [[H] ++ VV]);
_ ->
do_print(T, uncover(H, V, R))
end.
%% 多值拆包
uncover(_, [], R) -> R;
uncover(Name, [H | T], R) ->
uncover(Name, T, R ++ [[Name] ++ H]).
%% 遍历整个邮箱对
do([], V) -> V;
do([H | T], All) ->
[Name | Adds] = H,
case lists:keyfind(Name, 1, All) of
{_Name, V} ->
NV = check(Adds, V, []),
NAll = lists:keyreplace(Name, 1, All, {Name, NV}),
do(T, NAll);
_ ->
do(T, All ++ [{Name, [lists:sort(remove_duplicate(Adds))]}])
end.
%% 检查需要合并的账户,注意每一步都要去重且排序
check(Add, [], V) -> V ++ [lists:sort(remove_duplicate(Add))];
check(Add, [H | T], V) ->
L = [X || X <- H, Y <- Add, X == Y],
case length(L) == 0 of
true -> check(Add, T, V ++ [lists:sort(remove_duplicate(H))]);
_ ->
%% V ++ [lists:sort(remove_duplicate(Add ++ H))] ++ T
MergeV = lists:sort(remove_duplicate(Add ++ H)),
check(MergeV, V ++ T, [])
end.
%% list去重
remove_duplicate([]) -> [];
remove_duplicate(L = [_ | _]) -> sets:to_list(sets:from_list(L)).
提交测试
还行,凹过了47个例子,看来已经没有优化的空间,
我们的合并逻辑复杂度那是肉眼可见(n平方石锤),但是为了应对David这种例子又不得不一直重复遍历
[["David", "David0@m.co", "David1@m.co"],
["David", "David3@m.co", "David4@m.co"],
["David", "David4@m.co", "David5@m.co"],
["David", "David2@m.co", "David3@m.co"],
["David", "David1@m.co", "David2@m.co"]]
既然优化不了那我们只能尝试用官方推荐的并查集算法了,用Erlang改良一下
思路2
要把相同邮箱的账户合并,首先要找到相同的用户,但又不能根据名字来(重名),所以只能用邮箱来标记是否为同一个人,思考后思路如下,我们先遍历所有账户,把每一个邮箱自身作为一个键,且对应一个值,这个值指向同一堆的父节点(同一个账户下的所有邮箱为同一个节点),然后遍历所有树,如果有相同父节点的两堆数据我们直接合并他,这就是并查集的思想
并查集实现1
先用list实现,对应的就是[{key,value}],其中key作为index的作用
%% 首先初始化一个并查集,且每个元素都是自己的根节点
union_init(N) ->
L = lists:seq(1, N),
[{X, X} || X <- L].
%% 查找接口,如果查找的节点对应值为自己则表示自己就是根节点,如果不是,继续找他对应的节点的根节点,也就是找归属地环节
union_find(Index, Parents) ->
case lists:keyfind(Index, 1, Parents) of
{Index, Index} -> Index;
{_, Value} -> union_find(Value, Parents)
end.
%% 合并两颗树,将一棵树对应的父节点赋给另一颗树,即将两棵树标记为一棵树
union(Index1, Index2, Parents) ->
Parent1 = union_find(Index1, Parents),
Parent2 = union_find(Index2, Parents),
lists:keyreplace(Parent2, 1, Parents, {Parent2, Parent1}).
主要逻辑实现
accounts_merge(Accounts) ->
{EmailsCount, EmailToIndexMap} = build_emailToIndex_Map(Accounts, 1, maps:new()),
EmailToNameMap = build_emailToName_Map(Accounts, maps:new()),
%% 根据上面统计不相同邮箱的数量构建一个并查集
Parents = union_init(EmailsCount),
%% 合并相同树的操作
NewParents = union_email(Parents, Accounts, EmailToIndexMap),
%% 找到邮箱对应的索引,这里已经是树的索引了,同一颗树下的邮箱会处理到同一个索引下
IndexToEmailsMap =
build_indexToEmail_Map(maps:keys(EmailToIndexMap), NewParents, maps:new(), EmailToIndexMap),
build_res(maps:values(IndexToEmailsMap), EmailToNameMap, []).
%% 构造最后的答案格式输出,即名字+邮箱组合
build_res([], _EmailToNameMap, Res) -> Res;
build_res([H | T], EmailToNameMap, Res) ->
Emails = lists:sort(H),
[Email | _TName] = H,
Name = maps:get(Email, EmailToNameMap),
build_res(T, EmailToNameMap, [[Name | Emails] | Res]).
%% 构造邮箱的索引Map,即 #{1 -> 邮箱1}
build_indexToEmail_Map([], _Parents, IndexToEmailsMap, _EmailToIndexMap) -> IndexToEmailsMap;
build_indexToEmail_Map([H | T], Parents, IndexToEmailsMap, EmailToIndexMap) ->
Index = union_find(maps:get(H, EmailToIndexMap), Parents),
case maps:is_key(Index, IndexToEmailsMap) of
true ->
EmailList = maps:get(Index, IndexToEmailsMap),
NewMap = maps:put(Index, [H | EmailList], IndexToEmailsMap),
build_indexToEmail_Map(T, Parents, NewMap, EmailToIndexMap);
_ ->
NewMap = maps:put(Index, [H], IndexToEmailsMap),
build_indexToEmail_Map(T, Parents, NewMap, EmailToIndexMap)
end.
%% 遍历所有邮箱,相同账户下的邮箱作为一棵树,对应父节点为每个账户的第一个邮箱
union_email(Parents, [], _) -> Parents;
union_email(Parents, [[_Name | Emails] | T], EmailToIndexMap) ->
NewParents = do_union_email(Parents, Emails, EmailToIndexMap),
union_email(NewParents, T, EmailToIndexMap).
do_union_email(Parents, [], _EmailToIndexMap) -> Parents;
do_union_email(Parents, [First | T], EmailToIndexMap) ->
FirstIndex = maps:get(First, EmailToIndexMap),
do_union_email(Parents, FirstIndex, T, EmailToIndexMap).
do_union_email(Parents, _FirstIndex, [], _EmailToIndexMap) -> Parents;
do_union_email(Parents, FirstIndex, [H | T], EmailToIndexMap) ->
NextIndex = maps:get(H, EmailToIndexMap),
NewParents = union(FirstIndex, NextIndex, Parents),
do_union_email(NewParents, FirstIndex, T, EmailToIndexMap).
%% 构造邮箱-索引Map 即 #{邮箱1 -> 1},且计算所有邮箱的数量,注意相同邮箱不计算
build_emailToIndex_Map([], Count, Map) -> {Count, Map};
build_emailToIndex_Map([[_Name | Emails] | T], Count, Map) ->
{Count1, DoMap} = do_build_emailToIndex_Map(Emails, Count, Map),
build_emailToIndex_Map(T, Count1, DoMap).
do_build_emailToIndex_Map([], Count, Map) -> {Count, Map};
do_build_emailToIndex_Map([H | T], Count, Map) ->
case maps:is_key(H, Map) of
true -> do_build_emailToIndex_Map(T, Count, Map);
_ -> do_build_emailToIndex_Map(T, Count + 1, maps:put(H, Count, Map))
end.
%% 构造邮箱-账户名 Map 即 #{邮箱1 -> John}
build_emailToName_Map([], Map) -> Map;
build_emailToName_Map([[Name | Emails] | T], Map) ->
Map1 = do_build_emailToName_Map(Emails, Name, Map),
build_emailToName_Map(T, Map1).
do_build_emailToName_Map([], _Name, Map) -> Map;
do_build_emailToName_Map([Email | T], Name, Map) ->
case maps:is_key(Email, Map) of
true -> do_build_emailToName_Map(T, Name, Map);
_ -> do_build_emailToName_Map(T, Name, maps:put(Email, Name, Map))
end.
提交测试
?怎么比我暴力还慢,左思右想,参考了上次最小岛屿问题,发现是傻逼list性能过差,我们把list对应kv结构优化一下,改成map实现
并查集实现2
union_init(N) -> union_init(N, maps:new()).
union_init(0, Map) -> Map;
union_init(N, Map) ->
union_init(N - 1, maps:put(N, N, Map)).
union_find(Index, Parents) ->
case maps:get(Index, Parents) of
Index -> Index;
V -> union_find(V, Parents)
end.
union(Index1, Index2, Parents) ->
Parent1 = union_find(Index1, Parents),
Parent2 = union_find(Index2, Parents),
maps:put(Parent2, Parent1, Parents).
其他逻辑不用变,再次提交
提交测试
焯!下班
GNM Erlang