在“ The Haskell the Logic,Maths and Programming ”一书中,作者提出了两种替代方法,即找出k
的最小除数n
k > 1
,声称第二个版本比第一个版本快得多。我有理解为什么(我是初学者)的问题。
这是第一个版本(第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)
内通过 primes 列表进行迭代,而不是遍历该范围内的所有数字。
然而,这个论点并没有让我信服:递归函数ldpf
逐个吃掉素数列表allPrimes
中的数字。通过在所有整数列表上执行filter
来生成此列表。
因此,除非我遗漏了某些内容,否则第二个版本最终会迭代2..sqrt(n)
区间内的所有数字,但对于每个数字,它首先检查它是否为素数(相对昂贵的操作),如果是,它检查它是否划分n
(一个相对便宜的)。
我想说的是,检查每个k
的{{1}}除n
是否应该更快。我推理的缺陷在哪里?
答案 0 :(得分:8)
第二种解决方案的主要优点是您只计算一次素数列表allPrimes
。由于延迟评估,每个调用只计算它需要的素数,或者重用已经计算过的素数。所以昂贵的部分只计算一次,然后重新使用。
对于计算单个数字的最小除数,第一个版本确实更有效。但是,尝试运行ldp
和ld
来表示1和100000之间的所有数字,您会看到差异。
答案 1 :(得分:1)
haskell对我来说不得而知,如果没有对展位版本进行适当的测量,我只能假设声明是正确的。在这种情况下,原因可能是:
1.primes在某些数组中预先计算
2.在运行(和记忆)
上计算.primes3.primes是在运行时计算的(而不是记忆)
4.这只是猜测
[注]
答案 2 :(得分:1)
据我所知,除法操作并不像你想象的2的除数那么昂贵,这使得allPrimes
滤出的数字的一半被检查为“右移1位”就像计算操作一样简单,而第一种算法将通过整数来执行相对昂贵的真正除法。假设可能的除数是1956,它将被allPrimes
过滤掉,只需几乎免费执行第一次测试(右移将返回零 - 可被2整除),同时将2^4253-1
除以1956年已经毫无意义,因为它不能被2整除,并且如果真的有大数字,则需要花费很多时间,至少有一半(或者说5/6,对于除数2和3)是没用的。同样allPrimes
是一个缓存列表,因此检查要包含在allPrimes
中的素数的下一个整数仅使用经过验证的素数,因此即使对于实际素数,素数测试也不是非常昂贵。这种组合提供了第二种方法优势。