【问题标题】:F#: grouping by recurring sequences of elementsF#:按元素的重复序列分组
【发布时间】:2016-07-15 11:58:36
【问题描述】:

我有一个序列对(键,值),比如

[("a", 1), ("a", 2), ("a", 111), ("b", 3), ("bb", 1), ("bb", -1), ...]

,将其转换为类似序列的最有效方法是什么

[("a", [1,2,111]), ("b", [3]), ("bb", [1,-1])] 

或类似的?

该序列具有以下属性:非常大(>2Gb)

这使得 Seq.groupBy 真的无效且不正确,有没有其他方法可以做到?

P.S.:这个顺序:

[("a", 1), ("a", 2), ("a", 111), ("bb", 1), ("bb", -1), ("a", 5), ("a", 6), ...]

应该转换为

[("a", [1,2,111]), ("bb", [1,-1]), ("a", [5,6]), ...]

--

编辑#1:修正了不正确的样本

编辑 #2:序列很大,因此首选惰性(或最快)解决方案

【问题讨论】:

  • seq.groupby 怎么错了?
  • @JohnPalmer: groupBy 使用dictionary internally,我想这是 OP 想要避免的。他似乎在追求类似于uniq 的行为,其中只计算相邻的重复项。
  • @AntonSchwaighofer - groupby 可能不正确的原因有很多 - 我试图让 OP 说出适用于他的情况 -
  • @JohnPalmer groupBy 在这种情况下不正确,因为对于 [("a", 1), ("a", 2), ("a", 111), ("bb", 1 ), ("bb", -1), ("a", 5), ("a", 6), ...] 它将返回 [("a", [1,2,111, 5, 6]) , ("bb", [1,-1]), ...] 而不是 [("a", [1,2,111]), ("bb", [1,-1]), ("a" , [5,6]), ...]
  • @MarkSeemann 这个问题并不是真正的重复,因为它需要懒惰。此外,我们可以生成带有急切块的惰性序列。用Some包裹输入序列,打上None;使用前一个键和一个累加器作为状态馈送到Seq.scan。最后以Seq.choose 剥离状态。

标签: f# seq


【解决方案1】:

如果您想要获得惰性结果的选项,那么我认为没有保持可变状态的优雅方式。这是一个相对简单的突变。你维护你看到的最后一个键的存储,以及与之对应的所有值:

let s = [("a", 1); ("a", 2); ("a", 111); ("bb", 1); ("bb", -1); ("a", 5); ("a", 6)]
let s2 = 
    [
        let mutable prevKey = None
        let mutable values = System.Collections.Generic.List<_>()
        let init key value = 
            prevKey <- Some key
            values.Clear()
            values.Add value
        for (key, value) in s do
            match prevKey with
            | None -> init key value
            | Some k when k = key -> values.Add value
            | Some k -> 
                yield (k, List.ofSeq values)
                init key value
        match prevKey with
        | Some k -> yield (k, List.ofSeq values)
        | _ -> ()
    ]

这给出了:

val s2 : (string * int list) list =
  [("a", [1; 2; 111]); ("bb", [1; -1]); ("a", [5; 6])]

对于惰性求值,将[ ... ] 替换为seq { ... }

【讨论】:

  • 这种解决方案的问题在于它不是惰性求值的——它将继续读取序列直到它成为最后一个元素;您的版本或我的类似解决方案也是 slower 然后 Seq.groupBy (ofc 给出的答案不正确,但是...)
  • 另外这个版本有错误,正确的结果应该是:[("a", [1; 2; 111]); ("bb", [1; -1]); ("a", [5; 6])]
  • 已修复错误。如果你想偷懒,只需将封闭的[ ... ] 替换为seq { ... }
  • @AntonSchwaighofer 您的代码有错误并为我返回[("a", [1; 2; 111]); ("a", [1]); ("a", [-1; 5; 6])]。我建议修改
  • @Ringil:谢谢,这表明在文本编辑器中乱序代码可能会产生意想不到的后果......
【解决方案2】:

一种没有可变状态的简单递归方法。

let rec chunk inseq (accumelem,accumlist) = 
    match inseq with
    |(a,b)::c -> 
        match accumelem with
        |Some(t) -> if t=a then chunk c (accumelem,b::accumlist) else (t,accumlist)::(chunk c (Some(a),b::[]))
        |None -> chunk c (Some a,b::[])
    |[] ->         
        match accumelem with
        |Some(t) -> (t,accumlist)::[]
        |None -> []


chunk [("a", 1); ("a", 2); ("a", 111); ("bb", 1); ("bb", -1); ("a", 5);("a", 6)] (None,[])

val it : (string * int list) list =
     [("a", [111; 2; 1]); ("bb", [-1; 1]); ("a", [6; 5])]

【讨论】:

    【解决方案3】:

    这是一个递归解决方案:

    let test = [("a", 1); ("a", 2); ("a", 111); ("bb", 1); ("bb", -1); ("a", 5); ("a", 6)]
    
    let groupByAdjacentElements alist = 
        let rec group a groupAcc prevElement adjacentAcc =
            match a with
            | [] -> match adjacentAcc with
                    | [] -> groupAcc
                    | _ -> (prevElement, List.rev adjacentAcc)::groupAcc
            | (b, c)::tail -> if b = prevElement then
                                 group tail groupAcc prevElement (c::adjacentAcc)
                              else
                                 group tail ((prevElement, List.rev adjacentAcc)::groupAcc) b [c]
    
        group alist [] (fst alist.Head) []
        |> List.rev
    
    let b = groupByAdjacentElements test
    

    返回:[("a", [1; 2; 111]); ("bb", [1; -1]); ("a", [5; 6])]

    如果你想要惰性评估,你应该考虑尝试LazyList

    编辑:这是一个将 ExtCore 中的 LazyList 与已接受的解决方案进行比较的脚本。它会生成一个大文本文件,然后执行要求的转换。请注意,LazyList 以相反的顺序返回:

    open System.Diagnostics
    open System.IO
    open ExtCore
    
    let fileName = "Test.txt"
    let outFile = new StreamWriter(fileName)
    for i in [1..20000*300] do
        outFile.WriteLine("a,1")
        outFile.WriteLine("a,2")
        outFile.WriteLine("a,111")
        outFile.WriteLine("bb,1")
        outFile.WriteLine("bb,-1")
        outFile.WriteLine("a,5")
        outFile.WriteLine("a,6")
        outFile.WriteLine("c,8")
    outFile.Close()
    
    printfn "Finished Writing to File"
    
    let data = System.IO.File.ReadLines(fileName) 
                |> Seq.map (fun i -> let parts = i.Split(',')
                                     (parts.[0], parts.[1]))
    printfn "Finished Reading File"
    
    let s2 data = 
        [
            let mutable prevKey = None
            let mutable values = System.Collections.Generic.List<_>()
            let init key value = 
                prevKey <- Some key
                values.Clear()
                values.Add value
            for (key, value) in data do
                match prevKey with
                | None -> init key value
                | Some k when k = key -> values.Add value
                | Some k -> 
                    yield (k, List.ofSeq values)
                    init key value
            match prevKey with
            | Some key -> yield (key, List.ofSeq values)
            | _ -> ()
        ]
    
    let groupByAdjacentElements aseq = 
        let alist = LazyList.ofSeq aseq
        let rec group alist groupAcc prevElement adjacentAcc =
            match alist with
            | Cons((b, c), tail) -> 
                if b = prevElement then
                    group tail groupAcc prevElement (c::adjacentAcc)
                else
                    group tail (LazyList.consDelayed (prevElement, List.rev adjacentAcc) (fun () -> groupAcc)) b [c]
            | Nil -> 
                match adjacentAcc with
                | [] -> groupAcc
                | _ -> LazyList.consDelayed (prevElement, List.rev adjacentAcc) (fun () -> groupAcc)
    
    
        group alist LazyList.empty (fst (alist.Head())) []
    
    let groupByAdjacentElementsList aseq = 
        let alist = aseq |> Seq.toList
        let rec group a groupAcc prevElement adjacentAcc =
            match a with
            | [] -> match adjacentAcc with
                    | [] -> groupAcc
                    | _ -> (prevElement, List.rev adjacentAcc)::groupAcc
            | (b, c)::tail -> if b = prevElement then
                                 group tail groupAcc prevElement (c::adjacentAcc)
                              else
                                 group tail ((prevElement, List.rev adjacentAcc)::groupAcc) b [c]
    
        group alist [] (fst alist.Head) []
        |> List.rev
    
    [<EntryPoint>]
    let main argv =
        let stopwatch = new Stopwatch()
        stopwatch.Start()
        let b = s2 data
        printfn "The result is: %A" b
        stopwatch.Stop()
        printfn "It took %A ms." stopwatch.ElapsedMilliseconds
        System.GC.WaitForFullGCComplete() |> ignore
        stopwatch.Reset()
        stopwatch.Start()
        let b = groupByAdjacentElements data
        printfn "The result is: %A" b
        stopwatch.Stop()
        printfn "It took %A ms." stopwatch.ElapsedMilliseconds
        System.GC.WaitForFullGCComplete() |> ignore
        stopwatch.Reset()
        stopwatch.Start()
        let b = groupByAdjacentElementsList data
        printfn "The result is: %A" b
        stopwatch.Stop()
        printfn "It took %A ms." stopwatch.ElapsedMilliseconds
        0
    

    我在使用大小约为 300MB 的文件时,LazyListseq 解决方案稍慢(83 秒到 94 秒)。也就是说,与序列解决方案不同,LazyList 的主要优势在于对其进行迭代是缓存的。即使在执行 List.rev 时,普通的列表解决方案也比两者都快(没有它大约是 73 秒)。

    【讨论】:

      【解决方案4】:

      在没有可变绑定的情况下也可以通过相邻键进行分组。使用Seq.scan,可以生成带有急切块的惰性序列。它已经提供了一种特殊情况,即序列的第一个元素;通过将输入序列包装为选项后跟None,我们可以处理另一个。之后,我们跳过中间结果并用Seq.choose 去除状态。

      为了获得最大的多功能性,我建议使用类似于Seq.groupBy 的签名,

      f:('T -> 'Key) -> xs:seq<'T> -> seq<'Key * 'T list> when 'Key : equality
      

      它以一个关键的投影函数作为第一个参数。

      let chunkBy (f : 'T-> 'Key) xs =
          // Determine key and wrap in option
          seq{for x in xs -> Some(f x, x)
              // Indicates end of sequence
              yield None }
          |> Seq.scan (fun (_, acc, previous) current ->
              match previous, current with
              | Some(pKey, _), Some(key, value) when pKey = key ->
                  // No intermediate result, but add to accumulator
                  None, value::acc, current
              | _ ->
                  // New state is 3-tuple of previous key and completed chunk,
                  // accumulator from current element, and new previous element
                  Option.map (fun (k, _) -> k, List.rev acc) previous,
                  Option.map snd current |> Option.toList, current )
              (None, [], None)
          |> Seq.choose (fun (result, _, _) -> result)
      

      这可以通过还提供结果投影功能来满足OP的要求。

      let chunkBy2 (f : 'T-> 'Key) (g : 'T->'Result)  =
          chunkBy f >> Seq.map (fun (k, gs) -> k, List.map g gs)
      // val chunkBy2 :
      //   f:('T -> 'Key) -> g:('T -> 'Result) -> (seq<'T> -> seq<'Key * 'Result list>)
      //      when 'Key : equality
      
      ["a", 1; "a", 2; "a", 111; "b", 3; "bb", 1; "bb", -1]
      |> chunkBy2 fst snd
      // val it : seq<string * int list> =
      //   seq [("a", [1; 2; 111]); ("b", [3]); ("bb", [1; -1])]
      
      Seq.initInfinite (fun x ->
          if (x / 2) % 2 = 0 then "a", x else "b", x)
      |> chunkBy2 fst snd
      |> Seq.skip 50000
      // val it : seq<string * int list> =
      //   seq
      //     [("a", [100000; 100001]); ("b", [100002; 100003]); ("a", [100004; 100005]);
      //      ("b", [100006; 100007]); ...]
      

      【讨论】:

        猜你喜欢
        • 2018-03-26
        • 2013-06-06
        • 2015-08-18
        • 1970-01-01
        • 2021-04-29
        • 1970-01-01
        • 2012-03-13
        • 2021-08-25
        • 1970-01-01
        相关资源
        最近更新 更多