以下代码适用于小型列表,但是长列表需要永久使用,我认为这是我对长度的双重用法。
ratioOfPrimes :: [Int] -> Double
ratioOfPrimes xs = fromIntegral (length (filter isPrime xs))/ fromIntegral(length xs)
如何计算较长列表中元素的比例?
答案 0 :(得分:4)
length
的双重用法不是主要问题。您的实现中的多次遍历会产生一个常数因子,并且使用length
和filter
加倍,您会得到O(3n)
的平均复杂度。由于Stream Fusion,它甚至是O(2n)
,正如Impredicative已经提到的那样。但事实上,由于常数因素对性能没有显着影响,因此简单地忽略它们甚至是常规的,因此,从传统上讲,您的实现仍然具有O(n)
的复杂性,其中n
是输入列表的长度。
这里真正的问题是,只有当isPrime
具有O(1)
的复杂性时,上述情况才会成立,但事实并非如此。此函数执行遍历所有素数的列表,因此它本身具有O(m)
的复杂性。因此,这种戏剧性的性能下降是由于您的算法具有O(n*m)
的最终复杂度,因为在输入列表的每次迭代中,它必须遍历所有素数的列表到未知深度。
为了优化,我建议首先对输入列表进行排序(取O(n*log n)
)并在所有素数列表上迭代自定义查找,这将在每次迭代时删除已访问过的数字。通过这种方式,您将能够在所有素数列表上实现单个遍历,从理论上讲,这可以授予您O(n*log n + n + m)
的复杂性,这通常可以简单地认为是O(n*log n)
,通过突出成本中心。
答案 1 :(得分:3)
所以,那里有一些事情发生。让我们看一下所涉及的一些操作:
length
filter
isPrime
length
正如您所说,使用length
两次不会有帮助,因为列表的O(n)
。你这样做了两次。然后是filter
,这也将在O(n)
中完成整个列表的传递。我们想要做的是在列表的一次通过中完成所有这些。
Data.List.Stream
模块中的函数实现了一种名为Stream Fusion的技术,例如将(length (filter isPrime xs))
调用重写为单个循环。但是,你还有第二次调用长度。您可以使用一对累加器将整个事物重写为单个折叠(或使用State或ST monad),并在一次传递中执行此操作:
ratioOfPrimes xs = let
(a,b) = foldl' (\(odd,all) i -> if (isPrime i) then (odd +1, all+1) else (odd, all+1)) (0,0) xs
in a/b
但是,在这种情况下,您也可以不使用列表并使用vector库。矢量库实现了相同的流融合技术,用于删除中间列表,但也有一些其他漂亮的功能:
length
是O(1)
Data.Vector.Unboxed
模块允许您存储不可浏览的类型(当前是Int
这些基本类型)而没有盒装表示的开销。因此,这个整数列表将存储为低级Int
数组。使用vector
软件包可以让您编写上面的惯用代码,并且比单遍翻译的性能更好。
import qualified Data.Vector.Unboxed as U
ratioOfPrimes :: U.Vector Int -> Double
ratioOfPrimes xs = (fromIntegral $ U.length . U.filter isPrime $ xs) / (fromIntegral $ U.length xs)
当然,未提及的是isPrime
函数,以及真正的问题是它对于大n
来说是否缓慢。一个不太好的主要检查员可能很容易将对列表索引的担忧从水中消除。