Haskell:列表 vs 数组,性能差异

5

我是一个 Haskell 新手,想要比较解决 Project Euler 网站问题 #14 的各种方法的效率。特别地,我希望更好地理解四种(略有不同)解决该问题方法之间评估时间差异的因素。

首先,快速概述一下问题 #14。它与“Collatz 数”有关(即与我之前发帖探索的 Haskell 不同方面的编程练习相同)。给定整数的 Collatz 数等于该整数的 Collatz 序列的长度。对于整数的 Collatz 序列计算如下:序列中的第一个数字(“n0”)是该整数本身;如果 n0 是偶数,则序列中的下一个数字(“n1”)等于 n/2;如果 n0 是奇数,则 n1 等于 3 * n0 + 1。我们递归地扩展序列,直到到达 1,此时序列完成。例如,5 的 Collatz 序列为:{5、16、8、4、2、1}(因为 16 = 3 * 5 + 1,8 = 16/2,4 = 8/2,...)。

问题14要求我们找到小于100万的整数,其具有最大的 Collatz 数。为此,我们可以考虑一个函数“collatz”,当传递一个整数“n”作为参数时,返回具有最大 Collatz 数的小于 n 的整数。换句话说,p 1000000 给出了问题 #14 的答案。

为了了解评估时间差异(即理解不同之处),我们可以考虑 Haskell 版本的“collatz”,其在两个维度上有所不同:

(1) 实现:我们将 Collatz 数集(将生成所有整数1..n的Collatz数)存储为列表或数组?我称这为“实现”维度,即函数的实现为“列表”或“数组”。

(2) 算法:我们是否通过扩展 Collatz 序列来计算任何给定整数 n 的 Collatz 数,直到它完成(即直到达到1)?还是我们只扩展序列,直到我们达到小于 n 的数字 k(此时我们可以简单地使用 k 的 Collatz 数,我们已经计算过了)?我称这为“算法”维度,即函数的算法为“完整”(计算每个整数的 Collatz 数)或“部分”。后者显然需要更少的操作。

以下是“collatz”函数的四个可能版本:数组/部分,列表/部分,数组/完整和列表/完整:

import Data.Array ( (!) , listArray , assocs )
import Data.Ord ( comparing )
import Data.List ( maximumBy )

--array implementation; partial algorithm (FEWEST OPERATIONS)
collatzAP x = maximumBy (comparing snd) $ assocs a where
    a = listArray (0,x) (0:1:[c n n | n <- [2..x]])
    c n i = let z = if even i then div i 2 else 3*i+1
      in if i < n then a ! i else 1 + c n z

--list implementation; partial algorithm
collatzLP x = maximum a where   
    a = zip (0:1:[c n n | n <- [2..x]]) [0..x]
    c n i = let z = if even i then div i 2 else 3*i+1
      in if i < n then fst (a!!i) else 1 + c n z

--array implementation, complete algorithm
collatzAC x = maximumBy (comparing snd) $ assocs a where
    a = listArray (0,x) (0:1:[c n n | n <- [2..x]])
    c n i = let z = if even i then div i 2 else 3*i+1
     in if i == 1 then 1 else 1 + c n z     

--list implementation, complete algorithm (MOST OPERATIONS)
collatzLC x = maximum a where   
    a = zip (0:1:[c n n | n <- [2..x]]) [0..x]
    c n i = let z = if even i then div i 2 else 3*i+1
      in if i == 1 then 1 else 1 + c n z

关于评估速度:我知道数组比列表访问速度要快得多(即对于给定索引n,O(1) vs. O(n)访问时间),因此我期望“collatz”的“array”实现比“list”实现更快,其他条件相同。另外,我期望“partial”算法比“complete”算法更快(其他条件相同),因为它需要执行较少的操作才能构建Collatz数的数据集。
测试我们的四个函数在不同大小的输入上,我们观察到以下评估时间(下面有评论):
确实,“array/partial”版本是“collatz”的最快版本(差距很大)。但是,我发现“list/complete”不是最慢的版本有点违反直觉。“list/partial”荣膺最慢版本,比“list/complete”慢20倍以上!
我的问题是:'list/partial'和'list/complete'之间的评估时间差异(与'array/partial'和'array/complete'之间的差异相比)是否完全由于Haskell中列表和数组之间的访问效率差异引起?还是我没有执行“控制实验”(即是否存在其他因素)?

没有进行任何分析,但我强烈怀疑“list/partial”之所以慢,完全是由于索引效率低下。 - John L
1个回答

4

我不理解关于两个使用列表的算法相对性能如何与数组相关...但这是我的看法:

如果性能很重要,请尽量避免对长列表进行索引。正如你所知,索引实际上是一种遍历。 "List/partial" 是一个大量索引/遍历的过程,而 "List/complete" 则不然。因此,Array/complete 和 List/complete 之间的差异可以忽略不计,而 "list/partial" 和其他情况之间的差异则非常巨大。


你关于数组的相关性是正确的,因为我最初提出了这个问题 - 我已经相应地更新了它。对此很抱歉! - iceman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接