我最近和Haskell玩了很多,我想出了这个函数来找到 n 的素数:
nthPrime 1 = 2
nthPrime 2 = 3
nthPrime n = aux [2, 3] 3 5 n
where
aux knownPrimes currentNth suspect soughtNth =
let currentIsPrime = foldl (\l n -> l && suspect `mod` n /= 0)
True knownPrimes
in case (currentIsPrime, soughtNth == currentNth) of
(True, True) -> suspect
(True, False) -> aux (suspect:knownPrimes) (currentNth + 1)
(suspect + 2) soughtNth
_ -> aux knownPrimes currentNth (suspect + 2) soughtNth
我的问题是,有没有办法让累积参数(在这种情况下为knownPrimes
)不反转(如传递(suspect:knownPrimes)
时那样)?
我尝试使用knownPrimes ++ [suspect]
,但这似乎效率低下。
我的希望是,如果我能按顺序传递已知的素数,那么我可以进一步快速进行一些素数检查。
答案 0 :(得分:7)
在Haskell中,如果你使用累加器来构建一个列表,但最终不得不反转它,通常情况下最好放弃累加器而是生成列表 lazily 作为计算的结果。
如果你应用这种思维来搜索素数,并充分利用懒惰,你最终会得到一个众所周知的技术,即产生所有素数的无限列表。如果我们尽可能少地重构你的代码来使用这种技术,我们会得到类似的东西:
allPrimes = [2, 3] ++ aux 5
where
aux suspect =
let currentIsPrime = foldl (\l n -> l && suspect `mod` n /= 0) True
$ takeWhile (\n -> n*n <= suspect) allPrimes
in case currentIsPrime of
True -> suspect : aux (suspect + 2)
False -> aux (suspect + 2)
nthPrime n = allPrimes !! (n-1)
我已经删除了现在不必要的参数,并将代码从累积变为懒惰生成,并使用自己的结果作为主要除数的来源进行测试(这称为“打结”)。除此之外,这里唯一的变化是添加一个takeWhile
检查:因为我们测试除数的列表是根据自身定义的,并且无限启动,我们需要知道列表中的哪个位置< em>停止检查除数,这样我们就不会得到真正无限的递归。
除此之外,此代码效率低下:
foldl (\l n -> l && suspect `mod` n /= 0) True
不是检查列表中是否没有除数的好方法,因为是写的,一旦找到除数,它就不会停止,甚至虽然&&
本身就是快捷方式(一旦发现第一个参数为False
就停止)。
要允许正确的快捷方式,可以使用foldr
代替:
foldr (\n r -> suspect `mod` n /= 0 && r) True
或者,更好的是,使用预定义函数all
:
all (\n -> suspect `mod` n /= 0)
如果您使用all
并稍微重构它,它会是这样的:
allPrimes :: [Integer]
allPrimes = 2 : 3 : aux 5
where
aux suspect
| currentIsPrime = suspect : nextPrimes
| otherwise = nextPrimes
where
currentIsPrime =
all (\n -> suspect `mod` n /= 0)
$ takeWhile (\n -> n*n <= suspect) allPrimes
nextPrimes = aux (suspect + 2)
nthPrime :: Int -> Integer
nthPrime n = allPrimes !! (n-1)