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