提问人:jaakkoc 提问时间:2/10/2020 最后编辑:Frank Lexerjaakkoc 更新时间:2/18/2020 访问量:331
为什么此 F# 序列表达式是立方时间而不是线性时间?
Why is this F# sequence expression cubic time instead of linear?
问:
我在使用 时偶然发现了一种奇怪的时间复杂度行为。这是我能想到的最小案例来重现这一点。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)(如果我错了,请纠正我)。然而,据我所知,它有一个立方复杂性。seqIdWithUnfold
Seq.unfold
Seq.tryHead
Seq.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
是什么让这个操作的复杂性是立方的?
答:
5赞
Asti
2/10/2020
#1
seq
几乎总是.
aka 本质上是:O(n)
seq
IEnumerable<T>
type Enumerator<'a> = {
getNext : unit -> 'a option
}
type Seq<'a> = {
getEnumerator: unit -> Enumerator<'a>
}
每次计算序列时,都会创建一个捕获枚举状态的新序列。 然后重复调用,直到序列终止。Enumerator
getNext
您可以自己看到这一点,如果您在任何时候将 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 的。seqIdWithUnfold
Seq.tryHead
Seq.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.tail
Seq.tail
seq { (*loop yield*) }
评论
seq
Seq.init
Enumerable.Range
IEnumerable<>
O(N)
IList
.tail
Seq.tail
是 O(n) - github.com/dotnet/fsharp/blob/master/src/fsharp/FSharp.Core/...