Разделение списка в одинаковых размерах в Erlang

Я хочу разделить:

[1,2,3,4,5,6,7,8]

в

[[1,2],[3,4],[5,6],[7,8]]

Он отлично работает с:

[ lists:sublist(List, X, 2) || X <- lists:seq(1,length(List),2) ] .

Но это действительно так медленно. 10000 элементов занимает около 2,5 секунд на моем нетбуке. Я также написал очень быструю рекурсивную функцию, но мне просто интересно: может ли это понимание списка быть написано по-другому, так что это быстрее?

Ответ 1

Попробуйте следующее:

part(List) ->
        part(List, []).
part([], Acc) ->
        lists:reverse(Acc);
part([H], Acc) ->
        lists:reverse([[H]|Acc]);
part([H1,H2|T], Acc) ->
        part(T, [[H1,H2]|Acc]).

Тест в erlang-shell (я объявил эту функцию в модуле part):

2> part:part([1,2,3,4,5,6,7,8]).
[[1,2],[3,4],[5,6],[7,8]]
3> 
3> timer:tc(part, part, [lists:seq(1,10000)]).
{774,
 [[1,2],
  [3,4],
  [5,6],
  [7,8],
  "\t\n","\v\f",
  [13,14],
  [15,16],
  [17,18],
  [19,20],
  [21,22],
  [23,24],
  [25,26],
  [27,28],
  [29,30],
  [31,32],
  "!\"","#$","%&","'(",")*","+,","-.","/0","12","34",
  [...]|...]}

Всего 774 микросекунды (~ 0,8 миллисекунды)

Ответ 2

Вот два быстрых решения для вас, которые являются гибкими. Один легко читается, но только немного быстрее, чем предлагаемое решение. Другой довольно быстрый, но немного загадочный для чтения. И обратите внимание, что оба моих предложенных алгоритма будут работать для списков всего, а не только для числовых упорядоченных списков.

Вот "легко читаемый". Вызовите n_length_chunks(List,Chunksize). Например, чтобы получить список кусков длиной 2, вызовите n_length_chunks(List,2). Это работает для кусков любого размера, т.е. Вы можете вызвать n_length_chunks(List,4), чтобы получить [[1,2,3,4],[5,6,7,8],...]

n_length_chunks([],_) -> [];
n_length_chunks(List,Len) when Len > length(List) ->
    [List];
n_length_chunks(List,Len) ->
    {Head,Tail} = lists:split(Len,List),
    [Head | n_length_chunks(Tail,Len)].

Здесь намного быстрее, но определенно труднее читать и вызывается тем же способом: n_length_chunks_fast(List,2) (я сделал одно изменение к этому по сравнению с тем, что было выше, тем, что оно завершает конец список с undefined, если длина списка не будет делиться на чистую длину требуемой длины блока.

n_length_chunks_fast(List,Len) ->
  LeaderLength = case length(List) rem Len of
      0 -> 0;
      N -> Len - N
  end,
  Leader = lists:duplicate(LeaderLength,undefined),
  n_length_chunks_fast(Leader ++ lists:reverse(List),[],0,Len).

n_length_chunks_fast([],Acc,_,_) -> Acc;
n_length_chunks_fast([H|T],Acc,Pos,Max) when Pos==Max ->
    n_length_chunks_fast(T,[[H] | Acc],1,Max);
n_length_chunks_fast([H|T],[HAcc | TAcc],Pos,Max) ->
    n_length_chunks_fast(T,[[H | HAcc] | TAcc],Pos+1,Max);
n_length_chunks_fast([H|T],[],Pos,Max) ->
    n_length_chunks_fast(T,[[H]],Pos+1,Max).

Протестировано на моем (действительно старом) ноутбуке:

  • Ваше предлагаемое решение заняло около 3 секунд.
  • Мой медленный, но читаемый был немного быстрее и занимает около 1,5 секунд (все еще довольно медленно).
  • Моя быстрая версия занимает около 5 миллисекунд.
  • Для полноты решения Isac заняло около 180 миллисекунд на моей машине.

Изменить: ничего себе, мне нужно сначала прочитать полный вопрос. Хорошо, я буду продолжать здесь для потомков, если это поможет. Насколько я могу судить, нет хорошего способа сделать это, используя списки. Ваша исходная версия медленная, потому что каждая итерация sublist должна проходить каждый раз по списку, чтобы перейти к следующему X, что привело к сложности только под O (N ^ 2).

Ответ 3

Или со сгибом:

  lists:foldr(fun(E, []) -> [[E]]; 
                 (E, [H|RAcc]) when length(H) < 2 -> [[E|H]|RAcc] ;
                 (E, [H|RAcc]) -> [[E],H|RAcc]
              end, [], List).

Ответ 4

Вы можете сделать это следующим образом:

1> {List1, List2} = lists:partition(fun(X) -> (X rem 2) == 1 end, List).
{[1,3,5|...],[2,4,6|...]}
2> lists:zipwith(fun(X, Y) -> [X, Y] end, List1, List2).
[[1,2],[3,4],[5,6]|...]

Это займет ~ 73 миллисекунды с списком элементов 10000 на моем компьютере. Исходное решение занимает ~ 900 миллисекунд.

Но в любом случае я бы пошел с рекурсивной функцией.

Ответ 5

Я хочу представить немного сложное, но более гибкое (и в основном более быстрое) решение, предложенное @Tilman

split_list(List, Max) ->
    element(1, lists:foldl(fun
        (E, {[Buff|Acc], C}) when C < Max ->
            {[[E|Buff]|Acc], C+1};
        (E, {[Buff|Acc], _}) ->
            {[[E],Buff|Acc], 1};
        (E, {[], _}) ->
            {[[E]], 1}
    end, {[], 0}, List)).

поэтому функциональная часть может быть реализована как

part(List) ->
     RevList = split_list(List, 2),
     lists:foldl(fun(E, Acc) ->
         [lists:reverse(E)|Acc]
     end, [], RevList).

Обновление Я добавил реверс в случае, если вы хотите сохранить порядок, но, как я вижу, он добавляет не более 20% времени обработки.

Ответ 6

Вот более общий ответ, который работает с любым размером подсписок.

1> lists:foreach(fun(N) -> io:format("~2.10.0B -> ~w~n",[N, test:partition([1,2,3,4,5,6,7,8,9,10],N)]    ) end, [1,2,3,4,5,6,7,8,9,10]).
01 -> [[1],[2],[3],[4],[5],[6],[7],[8],[9],[10]]
02 -> [[1,2],[3,4],[5,6],[7,8],[9,10]]
03 -> [[1,2,3],[4,5,6],[7,8,9],[10]]
04 -> [[1,2,3,4],[5,6,7,8],[10,9]]
05 -> [[1,2,3,4,5],[6,7,8,9,10]]
06 -> [[1,2,3,4,5,6],[10,9,8,7]]
07 -> [[1,2,3,4,5,6,7],[10,9,8]]
08 -> [[1,2,3,4,5,6,7,8],[10,9]]
09 -> [[1,2,3,4,5,6,7,8,9],[10]]
10 -> [[1,2,3,4,5,6,7,8,9,10]]

И код для этого сохраняется в файле с именем test.erl:

-module(test).
-compile(export_all).

partition(List, N) ->
    partition(List, 1, N, []).

partition([], _C, _N, Acc) ->
    lists:reverse(Acc) ;

partition([H|T], 1, N, Acc) ->
    partition(T, 2, N, [[H]|Acc]) ;

partition([H|T], C, N, [HAcc|TAcc]) when C < N ->
    partition(T, C+1, N, [[H|HAcc]|TAcc]) ;

partition([H|T], C, N, [HAcc|TAcc]) when C == N ->
    partition(T, 1, N, [lists:reverse([H|HAcc])|TAcc]) ;

partition(L, C, N, Acc) when C > N ->
    partition(L, 1, N, Acc).

Возможно, он был бы более изящным в отношении частного случая, где C > N. Обратите внимание, что C - это размер создаваемого текущего подсписника. При запуске он равен 1. Затем он увеличивается до достижения размера раздела N.

Мы также могли использовать модифицированную версию кода @chops, чтобы последний список содержал остальные элементы, даже если его размер < N:

-module(n_length_chunks_fast).

-export([n_length_chunks_fast/2]).

n_length_chunks_fast(List,Len) ->
    SkipLength = case length(List) rem Len of
        0 -> 0;
        N -> Len - N
    end,
    n_length_chunks_fast(lists:reverse(List),[],SkipLength,Len).

n_length_chunks_fast([],Acc,_Pos,_Max) -> Acc;

n_length_chunks_fast([H|T],Acc,Pos,Max) when Pos==Max ->
    n_length_chunks_fast(T,[[H] | Acc],1,Max);

n_length_chunks_fast([H|T],[HAcc | TAcc],Pos,Max) ->
    n_length_chunks_fast(T,[[H | HAcc] | TAcc],Pos+1,Max);

n_length_chunks_fast([H|T],[],Pos,Max) ->
    n_length_chunks_fast(T,[[H]],Pos+1,Max).

Ответ 7

Я немного изменил реализацию из @JLarky, чтобы удалить выражение guard, которое должно быть немного быстрее:

split_list(List, Max) ->
    element(1, lists:foldl(fun
        (E, {[Buff|Acc], 1}) ->
            {[[E],Buff|Acc], Max};
        (E, {[Buff|Acc], C}) ->
            {[[E|Buff]|Acc], C-1};
        (E, {[], _}) ->
            {[[E]], Max}
    end, {[], Max}, List)).

Ответ 8

Я искал функцию раздела, которая могла бы разделить большой список на небольшое количество рабочих. С lkuty partition вы можете получить, что один рабочий получает почти двойную работу, чем все остальные. Если это не то, что вы хотите, вот версия, длина подрежима которой отличается не более чем на 1.

Использует PropEr для тестирования.

%% @doc Split List into sub-lists so sub-lists lengths differ most by 1.
%% Does not preserve order.
-spec split_many(pos_integer(), [T]) -> [[T]] when T :: term().
split_many(N, List) ->
    PieceLen = length(List) div N,
    lists:reverse(split_many(PieceLen, N, List, [])).

-spec split_many(pos_integer(), pos_integer(), [T], [[T]]) ->
    [[T]] when T :: term().
split_many(PieceLen, N, List, Acc) when length(Acc) < N ->
    {Head, Tail} = lists:split(PieceLen, List),
    split_many(PieceLen, N, Tail, [Head|Acc]);

split_many(_PieceLen, _N, List, Acc) ->
    % Add an Elem to each list in Acc
    {Appendable, LeaveAlone} = lists:split(length(List), Acc),
    Appended = [[Elem|XS] || {Elem, XS} <- lists:zip(List, Appendable)],
    lists:append(Appended, LeaveAlone).

Тесты:

split_many_test_() ->
    [
     ?_assertEqual([[1,2]], elibs_lists:split_many(1, [1,2])),
     ?_assertEqual([[1], [2]], elibs_lists:split_many(2, [1,2])),
     ?_assertEqual([[1], [3,2]], elibs_lists:split_many(2, [1,2,3])),
     ?_assertEqual([[1], [2], [4,3]], elibs_lists:split_many(3, [1,2,3,4])),
     ?_assertEqual([[1,2], [5,3,4]], elibs_lists:split_many(2, [1,2,3,4,5])),
     ?_assert(proper:quickcheck(split_many_proper1())),
     ?_assert(proper:quickcheck(split_many_proper2()))
    ].


%% @doc Verify all elements are preserved, number of groups is correct,
%% all groups have same number of elements (+-1)
split_many_proper1() ->
    ?FORALL({List, Groups},
            {list(), pos_integer()},
            begin
                Split = elibs_lists:split_many(Groups, List),

                % Lengths of sub-lists
                Lengths = lists:usort(lists:map(fun erlang:length/1, Split)),

                length(Split) =:= Groups andalso
                lists:sort(lists:append(Split)) == lists:sort(List) andalso
                length(Lengths) =< 2 andalso
                case Lengths of
                    [Min, Max] -> Max == Min + 1;
                    [_] -> true
                end
            end
           ).

%% @doc If number of groups is divisable by number of elements, ordering must
%% stay the same
split_many_proper2() ->
    ?FORALL({Groups, List},
            ?LET({A, B},
                 {integer(1, 20), integer(1, 10)},
                 {A, vector(A*B, term())}),
            List =:= lists:append(elibs_lists:split_many(Groups, List))
           ).