两个简单的代码来生成一个数的除数。为什么递归递增更快?

时间:2012-08-21 09:37:15

标签: performance haskell

在解决问题时,我必须计算一个数的除数。我有两个生成所有除数的实现> 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

2 个答案:

答案 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被迫至少计算一个中间列表元素,这完全禁用整个融合优化。

因此,第二种算法较慢,因为您生成了必须构造和销毁的中间列表,而递归算法只是立即生成一个列表。