【发布时间】:2014-03-25 03:02:13
【问题描述】:
在“The Haskell Road to Logic, Maths and Programming 一书中,作者提出了两种替代方法来找到数字n 和k > 1 的最小除数k,声称第二个版本比第一个版本快得多。我很难理解为什么(我是初学者)。
这是第一个版本(第 10 页):
ld :: Integer -> Integer -- finds the smallest divisor of n which is > 1
ld n = ldf 2 n
ldf :: Integer -> Integer -> Integer
ldf k n | n `rem` k == 0 = k
| k ^ 2 > n = n
| otherwise = ldf (k + 1) n
如果我理解正确,ld 函数基本上会遍历区间 [2..sqrt(n)] 中的所有整数,并在其中一个整数除以 n 时立即停止,并将其作为结果返回。
第二个版本,作者声称要快得多,如下所示(第 23 页):
ldp :: Integer -> Integer -- finds the smallest divisor of n which is > 1
ldp n = ldpf allPrimes n
ldpf :: [Integer] -> Integer -> Integer
ldpf (p:ps) n | rem n p == 0 = p
| p ^ 2 > n = n
| otherwise = ldpf ps n
allPrimes :: [Integer]
allPrimes = 2 : filter isPrime [3..]
isPrime :: Integer -> Bool
isPrime n | n < 1 = error "Not a positive integer"
| n == 1 = False
| otherwise = ldp n == n
作者声称此版本更快,因为它仅迭代区间 2..sqrt(n) 内的 素数 列表,而不是迭代该范围内的所有数字。
但是,这个论点并不能说服我:递归函数ldpf 正在一个接一个地吃掉素数列表allPrimes 中的数字。这个列表是通过对所有整数列表执行filter 生成的。
所以除非我遗漏了什么,否则第二个版本最终也会遍历区间 2..sqrt(n) 内的所有数字,但是对于每个数字,它首先检查它是否是素数(一个相对昂贵的操作),如果是,它检查它是否划分n(一个相对便宜的)。
我想说,仅检查 k 是否为每个 k 划分 n 应该更快。我的推理缺陷在哪里?
【问题讨论】:
-
可能是编译器的魔力适用于 isPrime,这使得它在大多数情况下比您预期的要快。
-
确实如此。与Petr Pudlák's answer 一样,对于单次使用,朴素算法应该击败仅素数算法。任何后续使用等于或低于先前使用会更快,因为
primes是一个常量应用形式(CAF)并且会被记忆。事实上,primes列表并不是最快的算法,但至少它避免了 Eratosthenes 多次删除问题的常见“不忠实”搜索。如果您想更快地找到素数,请阅读The Genuine Sieve of Eratosthenes。 -
与问题无关,我会使用
n*n而不是n^2,因为我不相信编译器会为我这样做。 -
会不会让第一个变体相对于第二个变体更具竞争力?即,检查 k=2,3,5,然后将 2 和 4 交替添加到 k 以遍历数字 6j-1 和 6j+1?
标签: performance algorithm haskell primes