Как разделить список с заданным размером группы?

Я ищу лучший способ разделить список (или seq), чтобы группы имели заданный размер. например допустим, я хочу сгруппировать с размером 2 (это может быть любое другое число):

let xs = [(a,b,c); (a,b,d); (y,z,y); (w,y,z); (n,y,z)]
let grouped = partitionBySize 2 input
// => [[(a,b,c);(a,b,d)]; [(y,z,y);(w,y,z)]; [(n,y,z)]]

Очевидным способом реализации partitionBySize было бы добавление позиции к каждому кортежу в списке ввода, чтобы он стал

[(0,a,b,c), (1,a,b,d), (2,y,z,y), (3,w,y,z), (4,n,y,z)]

а затем использовать GroupBy с

xs |> Seq.ofList |> Seq.GroupBy (function | (i,_,_,_) -> i - (i % n))

Однако это решение выглядит не очень элегантно для меня. Есть ли лучший способ реализовать эту функцию (может быть, с помощью встроенной функции)?

6 ответов

Решение

Вот хвостовая рекурсивная функция, которая перебирает список один раз.

let chunksOf n items =
  let rec loop i acc items = 
    seq {
      match i, items, acc with
      //exit if chunk size is zero or input list is empty
      | _, [], [] | 0, _, [] -> ()
      //counter=0 so yield group and continue looping
      | 0, _, _::_ -> yield List.rev acc; yield! loop n [] items 
      //decrement counter, add head to group, and loop through tail
      | _, h::t, _ -> yield! loop (i-1) (h::acc) t
      //reached the end of input list, yield accumulated elements
      //handles items.Length % n <> 0
      | _, [], _ -> yield List.rev acc
    }
  loop n [] items

использование

[1; 2; 3; 4; 5]
|> chunksOf 2 
|> Seq.toList //[[1; 2]; [3; 4]; [5]]

Мне нравится элегантность подхода Томаса, но я сравнил обе наши функции, используя входной список из 10 миллионов элементов. Этот показался в 9 секунд против 22 для его. Конечно, как он признал, наиболее эффективный метод, вероятно, будет включать массивы / циклы.

Кажется, это повторяющийся шаблон, который не фиксируется ни одной функцией в базовой библиотеке F#. При решении подобных проблем ранее я определил функцию Seq.groupWhen (см. фрагменты F#), который превращает последовательность в группы. Новая группа запускается, когда предикат верен.

Вы можете решить проблему, используя Seq.groupWhen аналогично Seq.group (начиная новую группу по четному индексу). В отличие от Seq.groupэто эффективно, потому что Seq.groupWhen перебирает входную последовательность только один раз:

[3;3;2;4;1;2;8] 
|> Seq.mapi (fun i v -> i, v) // Add indices to the values (as first tuple element)
|> Seq.groupWhen (fun (i, v) -> i%2 = 0) // Start new group after every 2nd element
|> Seq.map (Seq.map snd) // Remove indices from the values

Реализация функции напрямую с помощью рекурсии, вероятно, проще - решение от John делает именно то, что вам нужно, - но если вы хотите увидеть более общий подход, Seq.groupWhen может быть интересно

List.chunkBySize (подсказка: Скотт Влашин) теперь доступен и делает именно то, о чем вы говорите. Похоже, что новый с F# 4.0.

let grouped = [1..10] |> List.chunkBySize 3
// val grouped : int list list = 
//   [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]; [10]]

Seq.chunkBySize а также Array.chunkBySize также теперь доступны.

Как насчет рекурсивного подхода? - требуется только один проход

let rec partitionBySize length inp dummy = 
    match inp with
    |h::t ->
        if dummy |> List.length < length then
            partitionBySize length t (h::dummy)
        else dummy::(partitionBySize length t (h::[]))
    |[] -> dummy::[]

Затем вызвать его с partitionBySize 2 xs []

let partitionBySize size xs =
  let sq = ref (seq xs)
  seq {
    while (Seq.length !sq >= size) do
      yield Seq.take size !sq
      sq := Seq.skip size !sq
    if not (Seq.isEmpty !sq) then yield !sq
  }
  // result to list, if you want
  |> Seq.map (Seq.toList)
  |> Seq.toList

ОБНОВИТЬ

let partitionBySize size (sq:seq<_>) =
  seq {
    let e = sq.GetEnumerator()
    let empty = ref true;
    while !empty do
      yield seq { for i = 1 to size do
                    empty := e.MoveNext()
                    if !empty then yield e.Current
            }
  }

версия среза массива:

let partitionBySize size xs =
  let xa = Array.ofList xs
  let len = xa.Length
  [
    for i in 0..size..(len-1) do
      yield ( if i + size >= len then xa.[i..] else xa.[i..(i+size-1)] ) |> Array.toList 
  ]

Ну, я опоздал на вечеринку. Код ниже является хвостовой рекурсивной версией, использующей функции высокого порядка на List:

let partitionBySize size xs =
    let i = size - (List.length xs - 1) % size
    let xss, _, _ =
        List.foldBack( fun x (acc, ls, j) -> 
                                          if j = size then ((x::ls)::acc, [], 1) 
                                          else (acc, x::ls, j+1)
                       ) xs ([], [], i)
    xss

Я сделал тот же тест, что и Даниэль. Эта функция эффективна, хотя она в 2 раза быстрее, чем его подход на моей машине. Я также сравнил это с версией массива / цикла, они сравнимы с точки зрения производительности.

Более того, в отличие от ответа Джона, эта версия сохраняет порядок элементов во внутренних списках.

Другие вопросы по тегам