我在Project Euler #7的解决方案上反复出现堆栈溢出,我不明白为什么。 这是我的代码:
import System.Environment
checkPrime :: Int -> Bool
checkPrime n = not $ testList n [2..n `div` 2]
--testList :: Int -> [Int] -> Bool
testList _ [] = False
testList n xs
| (n `rem` (head xs) == 0) = True
| otherwise = testList n (tail xs)
primesTill n = sum [1 | x <- [2..n], checkPrime x]
nthPrime n = nthPrime' n 2
nthPrime' n x
| (primesTill x == n) = x
| otherwise = nthPrime' n x+1
main = print (nthPrime 10001)
答案 0 :(得分:2)
正如@bheklilr在他的评论中提到的,stackoverflow是由nthPrime'
函数的否则分支中的错误评估顺序引起的:
nthPrime' n x+1
将被解释为
(nthPrime' n x)+1
因为这个表达式是递归调用的,所以nthPrime' n 2
的调用将扩展为
(nthPrime' n 2)+1+1+1+1+1+1+1+1 ...
但是第二个参数永远不会增加,你的程序会收集大量未评估的thunk。仅当第一个参数减少到Int
时才会进行评估,但是您的函数处于无限递归状态,因此永远不会发生这种情况。所有加号都存储在堆栈中,如果没有剩余空间,您将收到堆栈溢出错误。
要解决此问题,您需要在x+1
周围放置parranteses,以便您的递归调用看起来像这样
nthPrime' n (x+1)
现在参数在传递给递归调用之前会递增。
这应解决您的stackoverflow问题,您可以尝试使用较小的数字,例如101
,您将获得所需的结果。
如果您使用原始值10001
测试您的程序,您可能会发现它仍然无法在合理的时间内完成。
我不会详细了解花哨算法来解决这些问题,如果您对它们感兴趣,可以轻松在线找到它们。 相反,我会向您展示代码中存在的问题,并向您展示一个简单的解决方案。
瓶颈是你的nthPrime
功能:
primesTill n = sum [1 | x <- [2..n], checkPrime x]
nthPrime n = nthPrime' n 2
nthPrime' n x
| (primesTill x == n) = x
| otherwise = nthPrime' n (x+1)
此函数检查2
和x
之间的素数是否等于n
。这个想法是正确的,但它会导致指数运行时。问题是您为每次迭代重新计算primesTill x
。要计算小于x
的素数,你要计算所有素数而不是总结它们。在x+1
的下一步中,您会忘记关于2
和x
之间数字的所有内容,如果它们仅作为素数,则再次测试它们,作为测试if {的最后一步{1}}是素数。比你重复一遍 - 忘记每件事并再次测试所有数字 - 直到你完成。
有很多可能性我会使用简单的无限列表,如果您对其他方法感兴趣,可以搜索术语x+1
或memoization
。
我们从您在dynamic programming
中使用的列表理解开始:
primesTill
这会计算 [1 | x <- [2..n], checkPrime x]
和2
之间的所有素数,但会立即忘记素数并将其替换为n
,因此第一步是保留实际数字。
1
这为我们提供了 [x | x <- [2..n], checkPrime x]
和2
之间所有素数的列表。如果我们有足够大的素数列表,我们可以使用索引函数n
来获得第10001个素数。所以我们需要将!!
设置为一个非常大的数字,以确保过滤后的列表足够长?
懒惰的救援评估!
haskell中的延迟评估允许我们构建一个无限列表,只根据需要进行评估。如果我们不为列表生成器提供上限,它将为我们构建这样一个无限列表。
n
现在我们有一个所有素数的无限列表。 我们可以将它绑定到一个名称,例如素数并用它来定义nthPrime
[x | x <- [2..], checkPrime x]
现在您可以使用primes = [x | x <- [2..], checkPrime x]
nthPrime n = primes !! n
进行编译,运行它,结果将立即发送给您。