【问题标题】:F# Split list into sublists based on comparison of adjacent elementsF#根据相邻元素的比较将列表拆分为子列表
【发布时间】:2011-01-17 18:09:52
【问题描述】:

我在 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 }

【问题讨论】:

  • 注意:this相关问题询问如何拆分并只保留pred = true元素
  • 交叉引用:here 是同一个问题,但对于序列。

标签: f# list


【解决方案1】:

进一步考虑了这一点,我想出了这个解决方案。我不确定它是否可读(除了我写的)。

更新在 Tomas 的回答中更好的匹配示例的基础上,这是一个改进的版本,它消除了“代码异味”(请参阅​​以前版本的编辑),并且可读性略高(我说)。

由于可怕的value restriction error,它仍然会在这个 (splitOn (&lt;&gt;) []) 上中断,但我认为这可能是不可避免的。

(编辑:修正了 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]]

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

【讨论】:

  • 它比我想出的函数更具可读性,但我会将loop (List.head ...etc 替换为match lst with | [] -&gt; [[]] | hd::tl -&gt; loop hd tl [] [],因此它不会在空输入时poof
  • @cfern,由于 #£$£~"! 值限制错误,无论如何都会在空列表上中断(就像 Tomas 的解决方案一样)。
  • @Benjol:您的解决方案在哪些方面会在空列表中中断? splitOn (fun a b -&gt; b - a &gt; 1) [] 给我[[]]...
  • @Benjol:您当前的解决方案存在“极端情况”的问题 - 它没有为[1; 3; 5;] 之类的输入提供正确的结果(当它应该给出[[1]; [3]; [5]] 时给出[[1]; [3; 5]]
  • @Johan Kullbom,很好发现!我现在已经修好了。同样对于您的第一条评论,我已经更新了我的答案。
【解决方案2】:

我更喜欢使用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)

【讨论】:

  • 您的解决方案的可读性不如我的。因为不是我写的! :)
【解决方案3】:

这是一个有趣的问题!我最近需要在 C# 中为我的 article about grouping 实现这一点(因为该函数的类型签名与 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

现在我们可以使用foldUntilEmpty 在输入列表上重复应用splitAt(将一些谓词指定为第一个参数),这为我们提供了我们想要的功能:

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# 核心库中的函数通用。

【讨论】:

  • @Tomas,很好。空列表给出值限制错误(对于我的解决方案也是如此)。我永远不确定如何处理它-> 我必须强制我的空列表的类型吗?另外,您对我最初的问题的另一半有什么想法 - 即在强制执行时如何初始化“prev”值以进行比较。
  • 要初始化 prev 值,您需要在循环结束时调用 MoveNext(请参阅我的 C# 文章中的操作方法)。 F# 不支持do .. while 循环,因此您需要使用显式递归来实现。
【解决方案4】:

怎么样:

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 消除了反转列表的需要。

【讨论】:

  • (欢迎来到 Stack Overflow)它的工作原理,现在让我明白了......!
【解决方案5】:

“相邻”立即让我想到 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]]

【讨论】:

    【解决方案6】:

    我喜欢 @Joh 和 @Johan 提供的答案,因为这些解决方案似乎是最惯用和最直接的。我也喜欢@Shooton 提出的一个想法。但是,每种解决方案都有自己的缺点。
    我试图避免:

    • 反转列表
    • 取消拆分并返回临时结果
    • 复杂的match 指令
    • 即使Seq.pairwise 似乎也是多余的
    • 使用下面的Unchecked.defaultof&lt;_&gt; 的成本可以删除空虚检查列表

    这是我的版本:

    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
    

    【讨论】:

      猜你喜欢
      • 2019-08-19
      • 2021-08-20
      • 1970-01-01
      • 1970-01-01
      • 2015-05-19
      • 1970-01-01
      • 2018-12-22
      • 2018-10-24
      • 1970-01-01
      相关资源
      最近更新 更多