在解决问题时,我必须计算一个数的除数。我有两个生成所有除数的实现> 1表示给定的数字。
第一种是使用简单的递归:
divisors :: Int64 -> [Int64]
divisors k = divisors' 2 k
where
divisors' n k | n*n > k = [k]
| n*n == k = [n, k]
| k `mod` n == 0 = (n:(k `div` n):result)
| otherwise = result
where result = divisors' (n+1) k
第二个使用Prelude中的列表处理函数:
divisors2 :: Int64 -> [Int64]
divisors2 k = k : (concatMap (\x -> [x, k `div` x]) $!
filter (\x -> k `mod` x == 0) $!
takeWhile (\x -> x*x <= k) [2..])
我发现第一个实现更快(我打印了返回的整个列表,因此不会因为懒惰而导致结果的任何部分未被评估)。这两个实现产生不同的有序除数,但这对我来说不是问题。 (事实上,如果k是一个完美的正方形,在第二个实现中,平方根输出两次 - 再次没问题)。
一般来说,Haskell中的这种递归实现更快吗?此外,我将不胜感激任何指针,使这些代码更快。谢谢!
编辑:
以下是我用于比较这两种性能实现的代码:https://gist.github.com/3414372
以下是我的时间测量:
使用divisor2进行严格评估($!)
$ ghc --make -O2 div.hs
[1 of 1] Compiling Main ( div.hs, div.o )
Linking div ...
$ time ./div > /tmp/out1
real 0m7.651s
user 0m7.604s
sys 0m0.012s
将divisors2与延迟评估($)一起使用:
$ ghc --make -O2 div.hs
[1 of 1] Compiling Main ( div.hs, div.o )
Linking div ...
$ time ./div > /tmp/out1
real 0m7.461s
user 0m7.444s
sys 0m0.012s
使用功能除数
$ ghc --make -O2 div.hs
[1 of 1] Compiling Main ( div.hs, div.o )
Linking div ...
$ time ./div > /tmp/out1
real 0m7.058s
user 0m7.036s
sys 0m0.020s
答案 0 :(得分:4)
因为你问,为了加快速度,应该使用不同的算法。简单明了的是首先找到一个素数因子,然后以某种方式从中构造除数。
标准prime factorization by trial division是:
factorize :: Integral a => a -> [a]
factorize n = go n (2:[3,5..]) -- or: `go n primes`
where
go n ds@(d:t)
| d*d > n = [n]
| r == 0 = d : go q ds
| otherwise = go n t
where (q,r) = quotRem n d
-- factorize 12348 ==> [2,2,3,3,7,7,7]
可以对等素因子进行分组和计算:
import Data.List (group)
primePowers :: Integral a => a -> [(a, Int)]
primePowers n = [(head x, length x) | x <- group $ factorize n]
-- primePowers = map (head &&& length) . group . factorize
-- primePowers 12348 ==> [(2,2),(3,2),(7,3)]
除数通常是按顺序构造的,但是:
divisors :: Integral a => a -> [a]
divisors n = map product $ sequence
[take (k+1) $ iterate (p*) 1 | (p,k) <- primePowers n]
因此,我们有
numDivisors :: Integral a => a -> Int
numDivisors n = product [ k+1 | (_,k) <- primePowers n]
这里的product
来自上面定义中的sequence
,因为列表monad sequence :: Monad m => [m a] -> m [a]
的{{1}}构建了一个元素的所有可能组合的列表每个成员列表m ~ []
,以便sequence_lists = foldr (\xs rs -> [x:r | x <- xs, r <- rs]) [[]]
和/或length . sequence_lists === product . map length
用于无限参数列表。
也可以进行有序生成:
length . take n === n
这个定义也很有效率,即它立即开始产生除数,没有明显的延迟:
ordDivisors :: Integral a => a -> [a]
ordDivisors n = foldr (\(p,k)-> foldi merge [] . take (k+1) . iterate (map (p*)))
[1] $ reverse $ primePowers n
foldi :: (a -> a -> a) -> a -> [a] -> a
foldi f z (x:xs) = f x (foldi f z (pairs xs)) where
pairs (x:y:xs) = f x y:pairs xs
pairs xs = xs
foldi f z [] = z
merge :: Ord a => [a] -> [a] -> [a]
merge (x:xs) (y:ys) = case (compare y x) of
LT -> y : merge (x:xs) ys
_ -> x : merge xs (y:ys)
merge xs [] = xs
merge [] ys = ys
{- ordDivisors 12348 ==>
[1,2,3,4,6,7,9,12,14,18,21,28,36,42,49,63,84,98,126,147,196,252,294,343,441,588,
686,882,1029,1372,1764,2058,3087,4116,6174,12348] -}
答案 1 :(得分:4)
递归版本通常不比基于列表的版本快。这是因为当计算遵循某种模式时,GHC编译器采用List fusion优化。这意味着列表生成器和“列表变换器”可能会被融合到一个大型生成器中。
但是,当您使用$!
时,基本上告诉编译器“请在执行下一步之前生成此列表的第一个缺点”。这意味着GHC被迫至少计算一个中间列表元素,这完全禁用整个融合优化。
因此,第二种算法较慢,因为您生成了必须构造和销毁的中间列表,而递归算法只是立即生成一个列表。