为什么此 F# 序列表达式是立方时间而不是线性时间?

Why is this F# sequence expression cubic time instead of linear?

提问人:jaakkoc 提问时间:2/10/2020 最后编辑:Frank Lexerjaakkoc 更新时间:2/18/2020 访问量:331

问:

我在使用 时偶然发现了一种奇怪的时间复杂度行为。这是我能想到的最小案例来重现这一点。Seq.unfold

let idUnfolder sequence =
   sequence
   |> Seq.tryHead
   |> Option.map (fun head -> (head, Seq.tail sequence))

let seqIdWithUnfold sequence =
   Seq.unfold idUnfolder sequence

该函数返回给定的序列本身。我希望得到的序列在线性时间内迭代,就像 O(n) 一样,并且是 O(1)(如果我错了,请纠正我)。然而,据我所知,它有一个立方复杂性。seqIdWithUnfoldSeq.unfoldSeq.tryHeadSeq.tail

我使用以下函数和一组值测试了执行时间。n

let test n =
   let start = System.DateTime.Now
   Seq.init n id
   |> seqIdWithUnfold
   |> Seq.iter ignore
   let duration = System.DateTime.Now - start
   printfn "%f" duration.TotalSeconds

是什么让这个操作的复杂性是立方的?

算法 调试 F# 时间复杂度 序列

评论

1赞 Panagiotis Kanavos 2/10/2020
A 本质上是一个 IEnumerable。这意味着对它的任何操作都必须再次迭代它,这反过来又意味着再次执行生成器/迭代器函数。 或者用于在不实际占用任何 RAM 的情况下生成一个,这意味着它们必须动态循环和生成值,这是一个操作。我怀疑如果您使用数组或 N 个项目的列表,您将获得更好的性能。seqSeq.initEnumerable.RangeIEnumerable<>O(N)
1赞 Panagiotis Kanavos 2/10/2020
顺便说一句,由于 Seq 是 IEnumerable<T>,因此可以将任何容器传递给需要 IEnumerabl<T> 的函数。一些操作实际上会检查它们是否适用于列表或数组(即实现容器)并优化它们的代码 - 它们不会走到第 i 个元素,而是返回第 i 个元素本身。对于链表,操作只需要返回第二个元素,而不是迭代整个序列来产生尾部。也许这也是优化的IList.tail
2赞 Phillip Carter 2/11/2020
Seq.tail是 O(n) - github.com/dotnet/fsharp/blob/master/src/fsharp/FSharp.Core/...

答:

5赞 Asti 2/10/2020 #1

seq几乎总是. aka 本质上是:O(n)seqIEnumerable<T>

type Enumerator<'a> = {
    getNext : unit -> 'a option
}

type Seq<'a> = {
    getEnumerator: unit -> Enumerator<'a>
}

每次计算序列时,都会创建一个捕获枚举状态的新序列。 然后重复调用,直到序列终止。EnumeratorgetNext

您可以自己看到这一点,如果您在任何时候将 a 替换为seq

source |> Seq.map(fun x -> printfn "Eval %A" x; x)

让我们也展示对以下内容的调用:getEnumerator

let sq  = 
    seq {  
        let mutable ctr = 0
        do printfn "Init  _" 
        while true do
            ctr <- ctr + 1
            printfn "Yield %d" ctr
            yield ctr
        }     

seqIdWithUnfold (sq |> Seq.take 3) |> Seq.toList |> printfn "%A"

输出如下:

Init  _
Yield 1
Init  _
Yield 1
Yield 2
Init  _
Yield 1
Yield 2
Yield 3
Init  _
Yield 1
Yield 2
Yield 3
[1; 2; 3]

这显示了经典模式。n(n+1)/2

您可以看到复杂度中将有 n + n2 项。

如果可以使用 list,则将得到 而不是 .O(n)O(n^2)

如果你真的想要,请使用数组。O(1)

评论

0赞 jaakkoc 2/10/2020
我明白了,所以当迭代 时,第 n 次计算需要计算尾巴的尾巴。(n 次)初始序列,因此复杂度为 n^2。我想知道是否有比使用可变的“枚举器”更好的解决方法(在我的用例中使用列表或数组是不可接受的)。我仍然想知道我是如何测量 n^3 的。seqIdWithUnfoldSeq.tryHeadSeq.unfold'State
0赞 Fyodor Soikin 2/10/2020
你能澄清一下你到底想做什么吗?您当前的示例返回序列本身,这是毫无意义的,因此我假设您试图解决的一定是更大的问题。为了回答是否有“更好的解决方法”,我们需要知道它的用途。
1赞 Asti 2/11/2020
@FyodorSoikin是对的。我假设他会在他的实现中使用 id 以外的映射。也许你可以问另一个问题?
1赞 Asti 2/11/2020
@jaakkoc 构建 n 枚举器的成本,通常在较长的序列中分摊。这可能使它在短序列的时序测试中看起来像 n^3。
1赞 jaakkoc 2/19/2020
我的最终解决方案是将 Seq 转换为 FSharpx.Collections.LazyList。我有许多函数需要通过递归传递序列的尾部。
0赞 jbtule 2/18/2020 #2

正如菲利普·卡特(Philip Carter)在评论中提到的是O(n)。Seq.tail

我不确定为什么 F# 列表是不可接受的,F# 列表是单链表,因此您可以获得恒定时间尾。如果你需要接受任何序列作为你的签名,只需在展开之前转换为列表,它仍然使它成为 O(n),你展示的这个例子无论如何都不能懒惰,除非你真的不需要尾巴。

let seqIdWithUnfold sequence =
       sequence
           |> Seq.toList
           |> List.unfold idUnfolder 
           |> List.toSeq

您的处理示例仍适用于 List 模块

 let idUnfolder listSeq =
        listSeq
        |> List.tryHead
        |> Option.map (fun head -> (head, List.tail listSeq))

但我认为它看起来会更干净一些

let idUnfolder =
    function | [] -> None
             | h::t -> Some(h, t);

Benchamarks

|          Method |        Mean |     Error |    StdDev |
|---------------- |------------:|----------:|----------:|
|        Original | 4,683.88 us | 36.462 us | 34.106 us |
|  ListConversion |    15.63 us |  0.202 us |  0.179 us |

// * Hints *
Outliers
  Benchmarkem.ListConversion: Default -> 1 outlier  was  removed (16.53 us)

// * Legends *
  Mean   : Arithmetic mean of all measurements
  Error  : Half of 99.9% confidence interval
  StdDev : Standard deviation of all measurements
  1 us   : 1 Microsecond (0.000001 sec)

评论

0赞 jaakkoc 2/19/2020
Seq.tail是 O(1),枚举它是 O(n)。在处理很长甚至无穷无尽的序列时,列表是不可接受的。
0赞 jbtule 2/19/2020
@jaakkoc,我明白了,因为是懒惰的,而且你唯一计划采用第一个元素,你说它应该是 O(1),但是,因为用一个跳过一个的可枚举来包装你的可枚举,并且每次展开迭代你都在用另一个跳过一个的可枚举来包装。因此,每一次展开都试图达到目的,它从原始序列的开始迭代(并且在内存中也在增长)。如果你不关心尾巴,也不想处理所有元素,那么你应该简单地使用一个 .Seq.tailSeq.tailseq { (*loop yield*) }