【发布时间】:2018-03-11 10:44:06
【问题描述】:
我有以下代码在 F# 中执行埃拉托色尼筛法:
let sieveOutPrime p numbers =
numbers
|> Seq.filter (fun n -> n % p <> 0)
let primesLessThan n =
let removeFirstPrime = function
| s when Seq.isEmpty s -> None
| s -> Some(Seq.head s, sieveOutPrime (Seq.head s) (Seq.tail s))
let remainingPrimes =
seq {3..2..n}
|> Seq.unfold removeFirstPrime
seq { yield 2; yield! remainingPrimes }
当primesLessThan 的输入远程很大时,这会非常慢:primes 1000000 |> Seq.skip 1000;; 对我来说需要将近一分钟,尽管primes 1000000 本身自然非常快,因为它只是一个序列。
我玩了一些,我认为罪魁祸首一定是Seq.tail(在我的removeFirstPrime)正在做一些密集的事情。 According to the docs,它正在生成一个全新的序列,我可以想象它很慢。
如果这是 Python 并且序列对象是一个生成器,那么确保此时不会发生任何代价高昂的事情将是微不足道的:只需来自序列的 yield,我们已经廉价地删除了它的第一个元素。
LazyList 在 F# doesn't seem 中具有 unfold 方法(或者,就此而言,filter 方法);否则我认为LazyList 会是我想要的。
如何通过防止不必要的重复/重新计算来加快实现速度?理想情况下,无论n 有多大,primesLessThan n |> Seq.skip 1000 都会花费相同的时间。
【问题讨论】:
-
it is。我用 n=10000、20000 或 100000 尝试了你的代码,观察到相同的运行时间。
-
但更改
primesLessThan n |> Seq.skip m |> Seq.take 10中的m确实会改变时间并显示它运行在大约 ~m^3 empirically,而不是理论上的 ~m^2 用于您的算法(产生 m 个素数)。当然,立方时间不是野餐。 :)(也是二次的) -
对于 a 解决方案,请参阅RosettaCode entry which runs at m^1.4 经验性地,在生产 的测试范围内m = 100K..200K 个素数。它使用不同的算法以及不同的定制类型。 (免责声明:我不是作者)。
-
@scrwtp 已经链接到一个这样说的答案,但我会在这里重复给任何发现这个问题的人:不要在递归代码中使用 Seq.tail。你可能认为因为 List.tail 是 O(1),所以 Seq.tail 也是 O(1)。它不是。 Seq.tail 是 O(N),会导致 O(N) 算法变为 O(N^2),或者 O(N^2) 算法变为 O(N^3) 等等。 (我看到你已经弄清楚了,但我想重复一遍,因为这是我在 seqs 中看到的第二个最常见的错误。第一个最常见的错误是忘记它们是懒惰的)。
-
@rmunn 事实上,我认为 Seq.tail 是 O(1) 的原因是因为 Python 中的类似概念是 O(1) - 但重点是,谢谢。
标签: f# sequence primes lazy-evaluation sieve-of-eratosthenes