我需要将排序列表合并到一个列表中(列表数量可能会有所不同)。当我刚开始使用Erlang时 - 我不知道函数lists:merge/1
。所以我实现了自己的merge/1
函数。它的复杂性是O(m * n)(m - 列表数,n - 列表中元素的平均数),我使用尾递归。这是我的功能:
-module( merge ).
-export( [ merge/1 ] ).
merge( ListOfLists ) ->
merge( ListOfLists, [] ).
merge( [], Merged ) ->
lists:reverse( Merged );
merge( ListOfLists, Merged ) ->
[ [ Hfirst | Tfirst ] | ListOfLists_Tail ] = ListOfLists,
% let's find list, which has minimal value of head
% result would be a tuple { ListWithMinimalHead, Remainder_ListOfLists }
{ [ Hmin | Tmin ], ListOfLists_WithoutMinimalHead } =
lists:foldl(
fun( [ Hi | Ti ] = IncomingList, { [ Hmin | Tmin ], Acc } ) ->
case Hi < Hmin of
true ->
% if incoming list has less value of head then swap it
{ [ Hi | Ti ], [ [ Hmin | Tmin ] | Acc ] };
false ->
{ [ Hmin | Tmin ], [ IncomingList | Acc ] }
end
end,
{ [ Hfirst | Tfirst ], [] },
ListOfLists_Tail ),
% add minimal-valued head to accumulator, and go to next iteration
case Tmin == [] of
true ->
merge( ListOfLists_WithoutMinimalHead, [ Hmin | Merged ] );
false ->
merge( [ Tmin | ListOfLists_WithoutMinimalHead ], [ Hmin | Merged ] )
end.
但是,在我了解lists:merge/1
之后 - 我决定测试我的解决方案的性能。
以下是一些结果:
1> c(merge).
{ok,merge}
2>
2>
3> timer:tc( lists, merge, [ [ lists:seq(1,N) || N <- lists:seq(1,5) ] ] ).
{5,[1,1,1,1,1,2,2,2,2,3,3,3,4,4,5]}
3>
3> timer:tc( merge, merge, [ [ lists:seq(1,N) || N <- lists:seq(1,5) ] ] ).
{564,[1,1,1,1,1,2,2,2,2,3,3,3,4,4,5]}
4>
4>
4> timer:tc( lists, merge, [ [ lists:seq(1,N) || N <- lists:seq(1,100) ] ] ).
{2559,
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1|...]}
5>
5> timer:tc( merge, merge, [ [ lists:seq(1,N) || N <- lists:seq(1,100) ] ] ).
{25186,
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1|...]}
6>
6>
6> timer:tc( lists, merge, [ [ lists:seq(1,N) || N <- lists:seq(1,1000) ] ] ).
{153283,
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1|...]}
7>
7> timer:tc( merge, merge, [ [ lists:seq(1,N) || N <- lists:seq(1,1000) ] ] ).
{21676268,
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1|...]}
8>
0.153秒给我留下了深刻的印象。 vs 21.676 sec。我的功能极其缓慢。
我认为使用匿名函数会降低性能,但摆脱fun
并没有帮助。
你能指出我,我犯了主要错误吗?或者为什么模块列表的功能更快?
由于
答案 0 :(得分:1)
不同之处在于算法的复杂性。 如果我没弄错的话,你的算法是 O(m ^ 2 * n),其中 n 是内部列表的长度, m 是输入列表中的内部列表编号。 这是因为您的函数有效地遍历内部列表的整个列表,以生成结果列表的一个元素。因此,对于您的测试示例,运行时间与 C1 * N ^ 3 成比例(其中C1是某些常数&lt; 1)。
然而,通常预先排序列表的合并操作具有 O(n)的复杂性,其中 n 是所有列表的总长度。因此,对于您的测试用例,复杂度应为 O(n * m),即它应与 C2 * N ^ 2 成比例。
事实上你可以看到你的测试中 N 增加了10倍,实现产生结果需要860倍的时间,而'lists:merge / 1'只需要53合并输入的时间更长。比率将根据实际输入大小和“形状”而有所不同,但总体趋势仍为N ^ 3对N ^ 2.
标准'lists:merge / 1'并不那么简单:https://github.com/erlang/otp/blob/maint/lib/stdlib/src/lists.erl#L1441('merge / 1'只调用'mergel / 1')但事实上即使是简单的,未优化的,也不是尾递归的“只需将头部列表与合并尾部合并”比您的实现更好:
merge2([]) ->
[];
merge2([Ls|Lss]) ->
merge2(Ls,merge2(Lss), []).
merge2([], Ls, Acc) ->
lists:reverse(Acc) ++ Ls;
merge2(Ls, [], Acc) ->
lists:reverse(Acc) ++ Ls;
merge2([H1|Ls1], [H2|_] = Ls2, Acc) when H1 =< H2 ->
merge2(Ls1, Ls2, [H1|Acc]);
merge2(Ls1, [H2|Ls2], Acc) ->
merge2(Ls1, Ls2, [H2|Acc]).
所以再一次,实际情况往往如此:任何优化的第一步都是看算法。
UPD:嗯,我的例子其实也是 O(m ^ 2 * n) - 在复杂性方面并不比你的好。我们可能需要的是“分而治之”的方法,它应该将复杂性提高到 O(m * n * ln(n))
UPD2:以前更新的更正和说明: 通过“分而治之”我的意思是以下算法:
假设我们的输入列表中有 m 排序列表,每个列表都包含 n 元素。然后:
此算法的渐近复杂度实际上是 O(n * m * ln(m)),因为: 1.在每个分层上拆分操作 O(m),因此可以忽略它。 2.合并操作在每个级别 O(m * n):在上部(第一个拆分)级别,我们需要合并两个列表 n * m / 2 O(n * m)的元素;在下一个级别(第二次拆分)我们需要做两个独立的合并,每个合并两个 n * m / 4 元素列表,这些元素也是 O(m * n)和依此类推,直到 m = 2 或 m = 1 3.级别数显然是 log2(m),因此产生的复杂性为 O(n * m * ln(m))
事实上,这个算法可以被认为只是merge sort的变体,它可以稍早“停止”分裂(因此它有 ln(m)而不是 ln(m * n) ))当 n = 1 (当你的第一个算法实际上变为selection sort时)它成为完整的合并排序