如何使用给定的组大小对列表进行分区?

时间:2011-11-09 11:00:08

标签: list f#

我正在寻找分区列表(或seq)的最佳方法,以便组具有给定的大小。 对于前假设我想要分组大小为2(这可能是任何其他数字):

let xs = [(a,b,c); (a,b,d); (y,z,y); (w,y,z); (n,y,z)]
let grouped = partitionBySize 2 input
// => [[(a,b,c);(a,b,d)]; [(y,z,y);(w,y,z)]; [(n,y,z)]]

实现partitionBySize的显而易见的方法是将位置添加到输入列表中的每个元组,以便它变为

[(0,a,b,c), (1,a,b,d), (2,y,z,y), (3,w,y,z), (4,n,y,z)]

然后使用GroupBy和

xs |> Seq.ofList |> Seq.GroupBy (function | (i,_,_,_) -> i - (i % n))

然而,这个解决方案对我来说并不是很优雅。 有没有更好的方法来实现这个功能(可能有内置功能)?

6 个答案:

答案 0 :(得分:9)

这似乎是F#核心库中任何函数都没有捕获的重复模式。在更早解决类似问题时,我定义了一个函数Seq.groupWhen(参见F# snippets),它将序列转换为组。当谓词成立时,将启动一个新组。

您可以使用Seq.groupWhenSeq.group类似地解决问题(通过在偶数索引处启动新组)。与Seq.group不同,这是有效的,因为Seq.groupWhen只对输入序列进行一次迭代:

[3;3;2;4;1;2;8] 
|> Seq.mapi (fun i v -> i, v) // Add indices to the values (as first tuple element)
|> Seq.groupWhen (fun (i, v) -> i%2 = 0) // Start new group after every 2nd element
|> Seq.map (Seq.map snd) // Remove indices from the values

使用递归直接实现函数可能更容易 - 来自John的解决方案正是您所需要的 - 但如果您想要查看更通用的方法,那么Seq.groupWhen可能会很有趣。

答案 1 :(得分:5)

List.chunkBySize(帽子提示:Scott Wlaschin)现已推出,正是您正在谈论的内容。它似乎是F#4.0的新功能。

let grouped = [1..10] |> List.chunkBySize 3
// val grouped : int list list = 
//   [[1; 2; 3]; [4; 5; 6]; [7; 8; 9]; [10]]

Seq.chunkBySizeArray.chunkBySize现在也可用。

答案 2 :(得分:4)

这是一个遍历列表的尾递归函数。

let chunksOf n items =
  let rec loop i acc items = 
    seq {
      match i, items, acc with
      //exit if chunk size is zero or input list is empty
      | _, [], [] | 0, _, [] -> ()
      //counter=0 so yield group and continue looping
      | 0, _, _::_ -> yield List.rev acc; yield! loop n [] items 
      //decrement counter, add head to group, and loop through tail
      | _, h::t, _ -> yield! loop (i-1) (h::acc) t
      //reached the end of input list, yield accumulated elements
      //handles items.Length % n <> 0
      | _, [], _ -> yield List.rev acc
    }
  loop n [] items

用法

[1; 2; 3; 4; 5]
|> chunksOf 2 
|> Seq.toList //[[1; 2]; [3; 4]; [5]]

我喜欢托马斯方法的优雅,但我使用1000万个元素的输入列表对我们的两个函数进行了基准测试。对于他来说,这个时间为9秒vs 22。当然,正如他所承认的,最有效的方法可能涉及数组/循环。

答案 3 :(得分:3)

递归方法怎么样? - 只需要一次通过

let rec partitionBySize length inp dummy = 
    match inp with
    |h::t ->
        if dummy |> List.length < length then
            partitionBySize length t (h::dummy)
        else dummy::(partitionBySize length t (h::[]))
    |[] -> dummy::[]

然后使用partitionBySize 2 xs []

调用它

答案 4 :(得分:2)

let partitionBySize size xs =
  let sq = ref (seq xs)
  seq {
    while (Seq.length !sq >= size) do
      yield Seq.take size !sq
      sq := Seq.skip size !sq
    if not (Seq.isEmpty !sq) then yield !sq
  }
  // result to list, if you want
  |> Seq.map (Seq.toList)
  |> Seq.toList

<强>更新

let partitionBySize size (sq:seq<_>) =
  seq {
    let e = sq.GetEnumerator()
    let empty = ref true;
    while !empty do
      yield seq { for i = 1 to size do
                    empty := e.MoveNext()
                    if !empty then yield e.Current
            }
  }

数组切片版本:

let partitionBySize size xs =
  let xa = Array.ofList xs
  let len = xa.Length
  [
    for i in 0..size..(len-1) do
      yield ( if i + size >= len then xa.[i..] else xa.[i..(i+size-1)] ) |> Array.toList 
  ]

答案 5 :(得分:1)

嗯,我参加晚会的时间已经很晚了。下面的代码是使用List上的高阶函数的尾递归版本:

let partitionBySize size xs =
    let i = size - (List.length xs - 1) % size
    let xss, _, _ =
        List.foldBack( fun x (acc, ls, j) -> 
                                          if j = size then ((x::ls)::acc, [], 1) 
                                          else (acc, x::ls, j+1)
                       ) xs ([], [], i)
    xss

我和丹尼尔做了同样的基准测试。这个功能很有效,但比我在机器上的方法快2倍。我还将它与数组/循环版本进行了比较,它们在性能方面具有可比性。

此外,与John的回答不同,此版本保留了内部列表中元素的顺序。