从头开始合并列表

时间:2012-08-15 23:22:09

标签: performance algorithm list merge erlang

我需要将排序列表合并到一个列表中(列表数量可能会有所不同)。当我刚开始使用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并没有帮助。

你能指出我,我犯了主要错误吗?或者为什么模块列表的功能更快?

由于

1 个答案:

答案 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 元素。然后:

  1. 将输入列表拆分为两个子列表,每个
  2. 中包含 m / 2 列表
  3. 以递归方式对每个算法应用此算法。
  4. 使用标准的2列表合并合并两个生成的排序列表。
  5. 此算法的渐近复杂度实际上是 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时)它成为完整的合并排序