【问题标题】:Take N elements from sequence with N different indexes in F#从 F# 中具有 N 个不同索引的序列中获取 N 个元素
【发布时间】:2011-03-20 09:09:17
【问题描述】:

我是 F# 的新手,正在寻找一个采用 N*indexes 和一个序列并给我 N 个元素的函数。如果我有 N 个索引,它应该等于 concat Seq.nth index0, Seq.nth index1 .. Seq.nth indexN 但它应该只扫描序列中的 indexN 个元素 (O(N)) 而不是 index0+index1+.. .+indexN (O(N^2)).

总而言之,我正在寻找类似的东西:

//For performance, the index-list should be ordered on input, be padding between elements instead of indexes or be ordered when entering the function
seq {10 .. 20} |> Seq.takeIndexes [0;5;10] 
Result: 10,15,20

我可以通过使用 seq { yield... } 来做到这一点,并有一个索引计数器在应该传递某些元素时进行标记,但如果 F# 提供了一个很好的标准方式,我宁愿使用它。

谢谢:)...

补充:我做了以下。它有效,但不漂亮。欢迎提出建议

let seqTakeIndexes (indexes : int list) (xs : seq<int>) =
    seq {
        //Assume indexes is sorted
        let e = xs.GetEnumerator()
        let i = ref indexes 
        let curr = ref 0

        while e.MoveNext() && not (!i).IsEmpty do
            if !curr = List.head !i then
                i := (!i).Tail
                yield e.Current

            curr := !curr + 1
    }

【问题讨论】:

  • 您的索引是否有序(即从最小到最大或相反)?
  • 只是想知道,但是您正在编写什么样的程序需要对您的序列进行索引访问?
  • Pavel:我们可以说它们是有序的。朱丽叶:实际上,我已经解决了欧拉计划问题 40,并且可以通过纯数学来解决。但我想让我的功能解决方案看起来更好:)
  • 对于它的价值,seq 并不容易分解,您有时需要使用命令式代码来处理 Seq 模块不能很好地处理的情况。从使用您的代码的客户的角度来看,您所拥有的已经是一个“纯”功能,并且可以满足您的特定需求。

标签: f# sequence indexing


【解决方案1】:

当您想通过索引访问元素时,使用序列并不是一个好主意。序列被设计为允许顺序迭代。我会将序列的必要部分转换为数组,然后按索引选择元素:

let takeIndexes ns input = 
  // Take only elements that we need to access (sequence could be infinite)
  let arr = input |> Seq.take (1 + Seq.max ns) |> Array.ofSeq
  // Simply pick elements at the specified indices from the array
  seq { for index in ns -> arr.[index] }

seq [10 .. 20] |> takeIndexes [0;5;10]  

关于您的实现 - 我认为它不能变得更加优雅。在实现需要以交错方式从多个来源获取值的函数时,这是一个普遍的问题 - 没有优雅的编写方式!

但是,您可以使用这样的递归以函数式的方式编写它:

let takeIndexes indices (xs:seq<int>) = 
  // Iterates over the list of indices recursively
  let rec loop (xe:IEnumerator<_>) idx indices = seq {
    let next = loop xe (idx + 1)
    // If the sequence ends, then end as well
    if xe.MoveNext() then
      match indices with
      | i::indices when idx = i -> 
        // We're passing the specified index 
        yield xe.Current
        yield! next indices
      | _ -> 
        // Keep waiting for the first index from the list
        yield! next indices }
  seq {
    // Note: 'use' guarantees proper disposal of the source sequence
    use xe = xs.GetEnumerator()
    yield! loop xe 0 indices }

seq [10 .. 20] |> takeIndexes [0;5;10]  

【讨论】:

  • +1 谢谢 :) 在这种情况下,转换为数组是多余的。我需要索引 0,9,99,999,9999,99999,999999。在这种情况下,我自己的方法比数组转换更快,并且比第二种方法快得多。但是很高兴看到它的功能性解决方案,并且很高兴学习新东西。前“使用”
  • 在我的代码和你的代码seq [1 .. 3] 可以简化为{1 .. 3}
  • @lasseespeholt:是的,不需要使用seq。我要么写[ 1 .. 3 ](创建一个列表),要么写seq { 1 .. 3 },这是一个序列表达式。根据规范:research.microsoft.com/en-us/um/cambridge/projects/fsharp/…(第 58 页),seq 的使用是编写seq { ... } 形式的序列表达式时的约定。 (另请参见第 59 页的范围示例)。此约定确保“序列表达式”被理解为 F# 计算表达式的特例......
  • ...另外两个常用的符号是[ ... ],用于创建应转换为 F# 列表的序列和[| ... |],用于创建应转换为数组的序列。
  • 教育文档(阅读 Chris Smith 的 Programming F#),谢谢 :)
【解决方案2】:

当你需要扫描一个序列并在O(n)中累积结果时,你总是可以回退到Seq.fold:

let takeIndices ind sq =
    let selector (idxLeft, currIdx, results) elem =
        match idxLeft with
            | []                               -> (idxLeft, currIdx, results)
            | idx::moreIdx when idx =  currIdx -> (moreIdx, currIdx+1, elem::results)
            | idx::_       when idx <> currIdx -> (idxLeft, currIdx+1, results)
            | idx::_                           -> invalidOp "Can't get here."
    let (_, _, results) = sq |> Seq.fold selector (ind, 0, [])
    results |> List.rev

seq [10 .. 20] |> takeIndices [0;5;10]

这种解决方案的缺点是它会将序列枚举到最后,即使它已经积累了所有需要的元素。

【讨论】:

  • 1+ 好吧,如果我需要它,我会尝试它:) 但在这种情况下,我希望它应该适用于无限序列。对于没有在问题中澄清这一点,我深表歉意。
【解决方案3】:

这是我的想法。此解决方案只会根据需要进入序列并将元素作为列表返回。

let getIndices xs (s:_ seq) =
    let enum = s.GetEnumerator()
    let rec loop i acc = function
        | h::t as xs ->
            if enum.MoveNext() then
                if i = h then
                    loop (i+1) (enum.Current::acc) t
                else
                    loop (i+1) acc xs
            else
                raise (System.IndexOutOfRangeException())
        | _ -> List.rev acc
    loop 0 [] xs

[10..20]
|> getIndices [2;4;8]
// Returns [12;14;18]

这里所做的唯一假设是您提供的索引列表已排序。否则该功能将无法正常工作。

【讨论】:

  • 1+ 很酷,就我而言,我认为返回一个列表会提供更好的性能,因为我只需要大量元素中的几个元素(但在这种情况下,差异并不重要)。但是,我最初认为返回一个序列更有意义,因为我使用的是序列。
【解决方案4】:

返回的结果排序有问题吗? 该算法将在输入序列上线性工作。只需要对索引进行排序。如果序列很大,但索引不是很多 - 它会很快。 复杂度是:N -> Max(indices),M -> 索引计数:最坏情况下为 O(N + MlogM)。

let seqTakeIndices indexes = 
    let rec gather prev idxs xs =
        match idxs with
        | [] -> Seq.empty
        | n::ns ->  seq { let left = xs |> Seq.skip (n - prev)
                          yield left |> Seq.head
                          yield! gather n ns left }
    indexes |> List.sort |> gather 0

这是一个 List.fold 变体,但阅读起来更复杂。我更喜欢第一个:

let seqTakeIndices indices xs = 
    let gather (prev, xs, res) n =
        let left = xs |> Seq.skip (n - prev)
        n, left, (Seq.head left)::res
    let _, _, res = indices |> List.sort |> List.fold gather (0, xs, [])
    res

附加:仍然比您的变体慢,但比我的旧变体快很多。因为没有使用 Seq.skip,它会创建新的枚举器并且会大大降低速度。

let seqTakeIndices indices (xs : seq<_>) = 
    let enum = xs.GetEnumerator()
    enum.MoveNext() |> ignore
    let rec gather prev idxs =  
        match idxs with
        | [] -> Seq.empty
        | n::ns -> seq { if [1..n-prev] |> List.forall (fun _ -> enum.MoveNext()) then 
                            yield enum.Current
                            yield! gather n ns }
    indices |> List.sort |> gather 0

【讨论】:

  • 嗯,我做了一个使用 Seq.skip 等的解决方案,但与我现在所拥有的相比,它给了我非常糟糕的性能。我猜你每次分配 left 时都会创建另一个枚举器?
  • 有趣的是 Seq.skip 是一个缓慢的函数......我认为这些是内部优化的。
  • 在这里你可以找到一些答案:stackoverflow.com/questions/1306140/…
猜你喜欢
  • 2023-03-13
  • 2023-01-02
  • 2018-03-11
  • 1970-01-01
  • 2011-02-11
  • 2011-02-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多