F#基于相邻元素的比较将列表拆分为子列表

时间:2010-02-17 07:52:57

标签: f# list

我在hubFS上找到了this question,但它根据各个元素处理了拆分条件。我想根据相邻元素的比较进行拆分,因此类型如下所示:

val split = ('T -> 'T -> bool) -> 'T list -> 'T list list

目前,我正试图从Don的必要解决方案开始,但我无法弄清楚如何初始化和使用'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 个答案:

答案 0 :(得分:8)

这是一个有趣的问题!我需要在最近为我的article about grouping在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#核心库中的函数那样通用。

答案 1 :(得分:6)

怎么样:

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无需反转列表。

答案 2 :(得分:2)

进一步思考这个问题后,我想出了这个解决方案。我不确定它是否具有可读性(除了编写它的我)。

更新基于Tomas答案中更好的匹配示例,这是一个改进的版本,它消除了“代码味道”(请参阅​​以前版本的编辑),并且稍微更具可读性(我说)。

由于可怕的value restriction error,它仍会在此(splitOn (<>) [])上中断,但我认为这可能是不可避免的。

(编辑:更正了Johan Kullbom发现的错误,现在正好适用于[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]]

对此有任何想法,或者我的问题中的部分解决方案?

答案 3 :(得分:1)

我更希望使用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)

答案 4 :(得分:1)

“毗邻”立刻让我想到了Seq.pairwise。

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]]

答案 5 :(得分:0)

我喜欢@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