#list #f#
#Список #f#
Вопрос:
Я ищу наилучший способ разделения списка (или 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))
Однако это решение выглядит не очень элегантно для меня.
Есть ли лучший способ реализовать эту функцию (возможно, с помощью встроенной функции)?
Комментарии:
1. В вашем примере группа внутри списка (строка с комментариями) моделируется как кортеж. Я думаю, вы хотите, чтобы это был список, то есть Список внутри списка, а не кортеж, поскольку это было бы невозможно при статической типизации
2. Согласен с Анкуром. Более того, результат, который вы ожидаете, не является допустимым значением F #, потому что последний кортеж отличается от остальных. Все элементы в списке должны иметь одинаковый тип.
3. да, извините, именно это я и имел в виду, спасибо, что указали на ошибку. Я соответствующим образом отредактировал вопрос.
Ответ №1:
Похоже, это повторяющийся шаблон, который не фиксируется ни одной функцией в библиотеке ядра 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
Реализовать функцию напрямую, используя рекурсию, вероятно, проще — решение от Джона делает именно то, что вам нужно, — но если вы хотели увидеть более общий подход, то Seq.groupWhen
это может быть интересно.
Комментарии:
1. интересно. Наиболее эффективное решение на данный момент.
2. @FrancescoDeVittori Если вы ищете наиболее эффективное решение, то я думаю, что преобразование входных данных в массив и использование slice
arr.[i .. i n]
вfor
цикле будет наиболее эффективным вариантом.
Ответ №2:
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
также теперь доступны.
Ответ №3:
Вот хвостовая рекурсивная функция, которая проходит по списку один раз.
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 у его. Конечно, как он признал, наиболее эффективным методом, вероятно, были бы массивы / циклы.
Комментарии:
1. Я выберу этот, но и другие тоже довольно хороши. Спасибо всем!
Ответ №4:
Как насчет рекурсивного подхода? — требуется только один проход
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 []
Комментарии:
1. результат:[[(1, 2, 4); (1, 2, 3)]; [(5, 6, 7); (6, 7, 6)]], потерянные данные
2. @BLUEPIXY — Забыл вернуть накопитель — должно быть исправлено
3. очень приятно, единственным недостатком является то, что он не является хвостовым рекурсивным и запускается при достаточно большом вводе (который по совпадению у меня есть :-))
Ответ №5:
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
]
Комментарии:
1. Я действительно не понимаю, почему, но это довольно медленно с моим большим набором данных (намного медленнее, чем у Tomas groupWhen)
2. Это медленно, потому что
Seq.skip
неэффективно, особенно когда вы вызываете его в последовательности, которая уже является результатомSeq.skip
. Возвращаемая последовательность повторяет исходную с самого начала при каждом обращении к ней.
Ответ №6:
Ну, я опоздал на вечеринку. Приведенный ниже код представляет собой хвостовую рекурсивную версию, использующую функции высокого порядка на 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 раза быстрее, чем его подход на моей машине. Я также сравнил его с версией массива / цикла, они сопоставимы с точки зрения производительности.
Более того, в отличие от ответа Джона, эта версия сохраняет порядок элементов во внутренних списках.
Комментарии:
1. Я хотел бы увидеть ваш тест. В моем тесте из 10 МИЛЛИОНОВ элементов мой все еще на секунду быстрее.
2. Я получаю разные результаты для начальной и последующих итераций. С самого начала мой работает быстрее. При последующих запусках ваш работает быстрее (примерно на 3 секунды — все еще не в 2 раза). Не уверен, к чему это отнести. Это в FSI.
3. Есть идеи, почему эта функция сокращается с 10 секунд при новом сеансе FSI до ~ 6 секунд при последующих запусках? Моя функция остается практически постоянной (~ 9 секунд). Просто интересно, что может быть по-другому.
4. Результаты, которые я получил, взяты из теста с 1 миллионом элементов. Не знаю, почему есть некоторые промежутки между холодным запуском и последующими запусками. Может быть, есть какие-то хитрости, стоящие за реализацией List.foldBack?
5. Использование 1M против моих 10M объясняет несоответствие. Разрыв будет сокращаться с увеличением входных данных и в конечном итоге изменится в мою пользу, поскольку ваш возвращает список (результаты накапливаются в памяти), а мой возвращает последовательность (обеспечивая постоянное использование памяти).