F# разделить список на подсписки на основе сравнения смежных элементов
Я нашел этот вопрос на hubFS, но что обрабатывает критерии разделения на основе отдельных элементов. Я хотел бы разделить на основе сравнения соседних элементов, чтобы тип выглядел так:
val split = ('T -> 'T -> bool) -> 'T list -> 'T list list
В настоящее время я пытаюсь начать с императивного решения Дона, но я не могу понять, как инициализировать и использовать значение "prev" для сравнения. Фолд лучше идти?
//Don's solution for single criteria, copied from hubFS
let SequencesStartingWith n (s:seq<_>) =
seq { use ie = s.GetEnumerator()
let acc = new ResizeArray<_>()
while ie.MoveNext() do
let x = ie.Current
if x = n && acc.Count > 0 then
yield ResizeArray.to_list acc
acc.Clear()
acc.Add x
if acc.Count > 0 then
yield ResizeArray.to_list acc }
6 ответов
это интересная проблема! Мне нужно было реализовать именно это в C# совсем недавно для моего статьи о группировке (потому что сигнатура типа функции очень похожа на groupBy
, поэтому его можно использовать в запросе LINQ как group by
предложения). Реализация c# была довольно уродливой.
там должны быть способом выразить эту функцию, используя некоторые простые примитивы. Просто кажется, что библиотека F# не предоставляет никаких функции, которые подходят для этой цели. Мне удалось придумать две функции, которые кажутся в целом полезными и могут быть объединены вместе для решения этой проблемы, поэтому вот они:
// Splits a list into two lists using the specified function
// The list is split between two elements for which 'f' returns 'true'
let splitAt f list =
let rec splitAtAux acc list =
match list with
| x::y::ys when f x y -> List.rev (x::acc), y::ys
| x::xs -> splitAtAux (x::acc) xs
| [] -> (List.rev acc), []
splitAtAux [] list
val splitAt : ('a -> 'a -> bool) -> 'a list -> 'a list * 'a list
это похоже на то, что мы хотим достичь, но он разбивает список только на две части (что проще, чем разбить список несколько раз). Затем нам нужно будет повторить эту операцию, что можно сделать с помощью этой функции:
// Repeatedly uses 'f' to take several elements of the input list and
// aggregate them into value of type 'b until the remaining list
// (second value returned by 'f') is empty
let foldUntilEmpty f list =
let rec foldUntilEmptyAux acc list =
match f list with
| l, [] -> l::acc |> List.rev
| l, rest -> foldUntilEmptyAux (l::acc) rest
foldUntilEmptyAux [] list
val foldUntilEmpty : ('a list -> 'b * 'a list) -> 'a list -> 'b list
теперь мы можем многократно применять splitAt
(С некоторым предикатом, указанным в качестве первого аргумента) во входном списке с помощью foldUntilEmpty
, что дает нам функцию мы хотели:
let splitAtEvery f list = foldUntilEmpty (splitAt f) list
splitAtEvery (<>) [ 1; 1; 1; 2; 2; 3; 3; 3; 3 ];;
val it : int list list = [[1; 1; 1]; [2; 2]; [3; 3; 3; 3]]
Я думаю, что последний шаг-это очень приятно :-). Первые две функции довольно просты и могут быть полезны для других вещей, хотя они не такие общие, как функции из библиотеки F# core.
Как насчет:
let splitOn test lst =
List.foldBack (fun el lst ->
match lst with
| [] -> [[el]]
| (x::xs)::ys when not (test el x) -> (el::(x::xs))::ys
| _ -> [el]::lst
) lst []
foldBack устраняет необходимость в обратном списке.
подумав об этом немного дальше, я придумал это решение. Я не уверен, что это очень читабельно (за исключением меня, кто это написал).
обновление основываясь на лучшем примере соответствия в ответе Томаса, вот улучшенная версия, которая удаляет "запах кода" (см. изменения для предыдущей версии) и немного более читаема (говорит мне).
Он все еще ломается на этом (splitOn (<>) []
), из-за страшных ограничение значение ошибка, но я думаю, что это может быть неизбежно.
(EDIT: Исправлена ошибка, обнаруженная Йоханом Куллбомом, теперь работает правильно для [1;1;2;3]. Проблема заключалась в том, что в первом матче я пропустил два элемента, это означало, что я пропустил сравнение/проверку.)
//Function for splitting list into list of lists based on comparison of adjacent elements
let splitOn test lst =
let rec loop lst inner outer = //inner=current sublist, outer=list of sublists
match lst with
| x::y::ys when test x y -> loop (y::ys) [] (List.rev (x::inner) :: outer)
| x::xs -> loop xs (x::inner) outer
| _ -> List.rev ((List.rev inner) :: outer)
loop lst [] []
splitOn (fun a b -> b - a > 1) [1]
> val it : [[1]]
splitOn (fun a b -> b - a > 1) [1;3]
> val it : [[1]; [3]]
splitOn (fun a b -> b - a > 1) [1;2;3;4;6;7;8;9;11;12;13;14;15;16;18;19;21]
> val it : [[1; 2; 3; 4]; [6; 7; 8; 9]; [11; 12; 13; 14; 15; 16]; [18; 19]; [21]]
любые мысли об этом или частичное решение в моем вопросе?
Я бы предпочел, используя List.fold
более явной рекурсии.
let splitOn pred = function
| [] -> []
| hd :: tl ->
let (outer, inner, _) =
List.fold (fun (outer, inner, prev) curr ->
if pred prev curr
then (List.rev inner) :: outer, [curr], curr
else outer, curr :: inner, curr)
([], [hd], hd)
tl
List.rev ((List.rev inner) :: outer)
"смежный" сразу заставляет меня думать о Seq.попарно.
let splitAt pred xs =
if Seq.isEmpty xs then
[]
else
xs
|> Seq.pairwise
|> Seq.fold (fun (curr :: rest as lists) (i, j) -> if pred i j then [j] :: lists else (j :: curr) :: rest) [[Seq.head xs]]
|> List.rev
|> List.map List.rev
пример:
[1;1;2;3;3;3;2;1;2;2]
|> splitAt (>)
выдает:
[[1; 1; 2; 3; 3; 3]; [2]; [1; 2; 2]]
мне нравятся ответы, предоставленные @Joh и @Johan, поскольку эти решения кажутся наиболее идиоматичными и простыми. Мне также нравится идея, предложенная @Shooton. Однако у каждого решения были свои недостатки.
Я пытался избежать:
- реверсивный списки
- отменить разбиение и присоединение обратно к временным результатам
- комплекс
match
- инструкции - даже
Seq.pairwise
оказалось избыточным - список проверки для пустоты может быть удалены в стоимости использования
Unchecked.defaultof<_>
ниже
вот моя версия:
let splitWhen f src =
if List.isEmpty src then [] else
src
|> List.foldBack
(fun el (prev, current, rest) ->
if f el prev
then el , [el] , current :: rest
else el , el :: current , rest
)
<| (List.head src, [], []) // Initial value does not matter, dislike using Unchecked.defaultof<_>
|> fun (_, current, rest) -> current :: rest // Merge temporary lists
|> List.filter (not << List.isEmpty) // Drop tail element