我在C中使用了Eratosthenes的Sieve非常容易地回答了Project Euler Question 7,我没有遇到任何问题。
我对F#还很陌生,所以我尝试实现相同的技术
let prime_at pos =
let rec loop f l =
match f with
| x::xs -> loop xs (l |> List.filter(fun i -> i % x <> 0 || i = x))
| _ -> l
List.nth (loop [2..pos] [2..pos*pos]) (pos-1)
当pos&lt; 1000,但会因超出内存异常而在10000崩溃
然后我尝试将算法更改为
let isPrime n = n > 1 && seq { for f in [2..n/2] do yield f } |> Seq.forall(fun i -> n % i <> 0)
seq {for i in 2..(10000 * 10000) do if isPrime i then yield i} |> Seq.nth 10000 |> Dump
成功运行但仍需要几分钟。
如果我理解正确,第一个算法是尾部优化的,为什么会崩溃?我如何编写一个运行时间不到1分钟的算法(我有一台快速计算机)?
答案 0 :(得分:5)
看着你的第一次尝试
let prime_at pos =
let rec loop f l =
match f with
| x::xs -> loop xs (l |> List.filter(fun i -> i % x <> 0 || i = x))
| _ -> l
List.nth (loop [2..pos] [2..pos*pos]) (pos-1)
在每次循环迭代中,您将迭代并创建新列表。由于列表创建速度很慢而且您没有看到缓存带来的任何好处,因此速度非常慢。跳过了几个明显的优化,例如跳过偶数的因子列表。当pos=10 000
您尝试创建一个列表时,该列表将占用整数的10 000 * 10 000 * 4 = 400MB
和另外800MB
个指针(F#列表是链接列表)。此外,由于每个列表元素占用的内存非常少,因此GC开销可能会带来很大的开销。在该功能中,您可以创建一个新的熟悉大小的列表。因此,我不会对此导致OutOfMemoryException
感到惊讶。
看第二个例子,
let isPrime n =
n > 1 &&
seq { for f in [2..n/2] do yield f }
|> Seq.forall(fun i -> n % i <> 0)
在这里,问题非常相似,因为您要为正在测试的每个元素生成巨型列表。
我在这里写了一个非常快速的F#筛[{3}},它展示了如何更快地完成这项工作。
答案 1 :(得分:3)
正如John已经提到的,您的实现很慢,因为它会生成一些临时数据结构。
在第一种情况下,您正在构建一个列表,该列表需要在内存中完全创建,这会带来很大的开销。
在第二种情况下,您正在构建一个不消耗内存的延迟序列(因为它在迭代时是构建的),但它仍然会引入间接性,从而减慢算法速度。
在F#的大多数情况下,人们往往更喜欢可读性,因此使用序列是编写代码的好方法,但在这里你可能更关心性能,所以我要避免使用序列。如果要保持代码的相同结构,可以像这样重写isPrime
:
let isPrime n =
let rec nonDivisible by =
if by = 1 then true // Return 'true' if we reached the end
elif n%by = 0 then false // Return 'false' if there is a divisor
else nonDivisible (by - 1) // Otherwise continue looping
n > 1 && nonDivisible (n/2)
这只是将序列和forall
替换为递归函数nonDivisible
,当true
号码不能被2和{{1之间的任何数字整除“时,它返回n
}}。该函数首先检查两个终止案例,否则执行递归调用..
通过最初的实现,我能够在1.5秒内找到第1000个素数,并且使用新的素数,需要22ms。使用新实现找到第10000个素数在我的机器上需要3.2秒。