我正在学习Haskell,所以我尝试实现移动平均功能。这是我的代码:
mAverage :: Int-> [Int] -> [Float]
mAverage x a = [fromIntegral k / fromIntegral x | k <- rawAverage]
where
rawAverage = mAverage' x a a
-- First list contains original values; second list contains moving average computations
mAverage' :: Int -> [Int] -> [Int] -> [Int]
mAverage' 1 a b = b
mAverage' x a b = mAverage' (x - 1) a' b'
where
a' = init a
b' = zipWith (+) a' (tail b)
用户使用每个平均值的长度和值列表调用mAverage(例如mAverage 4 [1,2..100]
)。
然而,当我在输入mAverage 4 [1,2..100000]
上运行代码时,我得到ghci需要3.6秒(使用:set +s
)并使用一千兆字节的内存。这对我来说似乎效率很低,因为等效函数在Python中只需要几分之一秒。有什么办法可以让我的代码更有效率吗?
答案 0 :(得分:9)
如果您想了解新的内容,可以查看移动平均值问题的优秀解决方案。它是由我的一个学生写的,所以我不会要求作者身份。我非常喜欢它,因为它很短。这里唯一的问题是average
函数。已知这些功能很糟糕。相反,您可以使用Beautiful folds by Gabriel Gonzalez。是的,这个函数需要 O(k) 时间(其中k
是窗口的大小)来计算窗口的平均值(我发现它更好,因为你可以如果您尝试仅将新元素添加到窗口并减去最后一个,则面临浮点错误。哦,它也使用State
monad:)
{-# LANGUAGE UnicodeSyntax #-}
module MovingAverage where
import Control.Monad (forM)
import Control.Monad.State (evalState, gets, modify)
moving :: Fractional a ⇒ Int → [a] → [a]
moving n _ | n <= 0 = error "non-positive argument"
moving n xs = evalState (forM xs $ \x → modify ((x:) . take (n-1)) >> gets average) []
where
average xs = sum xs / fromIntegral n
答案 1 :(得分:5)
这是一个简单的基于列表的解决方案,虽然需要更多内存,但却是惯用且足够快的。
ListModel listModel = listModels.get(position);
此解决方案允许在移动窗口中使用任何函数而不是import Data.List (tails)
mavg :: Fractional b => Int -> [b] -> [b]
mavg k lst = take (length lst-k) $ map average $ tails lst
where average = (/ fromIntegral k) . sum . take k
。
以下解决方案不太普遍,但它在空间中是恒定的,似乎是最快的。
average
最后,使用一种Okasaki的持久功能队列来保持移动窗口的解决方案。在处理流数据时,例如管道或管道,它确实有意义。
import Data.List (scanl')
mavg :: Fractional b => Int -> [b] -> [b]
mavg k lst = map (/ fromIntegral k) $ scanl' (+) (sum h) $ zipWith (-) t lst
where (h, t) = splitAt k lst
正如在原始帖子的评论中提到的那样,不要使用mavg k lst = map average $ scanl' enq ([], take k lst) $ drop k lst
where
average (l,r) = (sum l + sum r) / fromIntegral k
enq (l, []) x = enq ([], reverse l) x
enq (l, (_:r)) x = (x:l, r)
进行性能分析。例如,您无法在ghci
中看到scanl'
的任何好处。
答案 2 :(得分:1)
这是您的解决方案。
这个想法是扫描两个列表,一个是平均窗口开始的,另一个是结束的列表。获取列表的尾部成本与扫描我们正在跳过的部分一样多,而且我们不会复制任何内容。 (如果窗口大小通常非常大,我们可以一次性计算remaining_data
并计算sum initial_data
。)
我们会根据评论中的描述生成一个部分和的列表,然后将它们除以窗口宽度以获得平均值。
虽然slidingAverage
计算偏向位置的平均值(窗口宽度向右),centeredSlidingAverage
计算居中平均值,使用左侧和右侧的半窗口宽度。
import Data.List (splitAt, replicate)
slidingAverage :: Int -> [Int] -> [Double] -- window size, source list -> list of averages
slidingAverage w xs = map divide $ initial_sum : slidingSum initial_sum xs remaining_data
where
divide = (\n -> (fromIntegral n) / (fromIntegral w)) -- divides the sums by window size
initial_sum = sum initial_data
(initial_data, remaining_data) = splitAt w xs
centeredSlidingAverage :: Int -> [Int] -> [Double] -- window size, source list -> list of averages
centeredSlidingAverage w xs = slidingAverage w $ left_padding ++ xs ++ right_padding
where
left_padding = replicate half_width 0
right_padding = replicate (w - half_width) 0
half_width = (w `quot` 2) -- quot is integer division
slidingSum :: Int -> [Int] -> [Int] -> [Int] -- window_sum before_window after_window -> list of sums
slidingSum _ _ [] = []
slidingSum window_sum before_window after_window = new_sum : slidingSum new_sum new_before new_after
where
value_to_go = head before_window
new_before = tail before_window
value_to_come = head after_window
new_after = tail after_window
new_sum = window_sum - value_to_go + value_to_come
当我尝试length $ slidingAverage 10 [1..1000000]
时,我的MBP只需不到一秒钟。 Due to the laziness,centeredSlidingAverage
大约需要同一时间。
答案 3 :(得分:0)
一种简单的方法,也使用O(n)复杂度
[user2@test ~]$ sudo su - user1
user2 Password:
[user1@test ~]$ sh script.sh
答案 4 :(得分:0)
另一种方法是使用STUArray。
import Data.Array.Unboxed
import Data.Array.ST
import Data.STRef
import Control.Monad
import Control.Monad.ST
movingAverage :: [Double] -> IO [Double]
movingAverage vals = stToIO $ do
let end = length vals - 1
myArray <- newArray (1, end) 0 :: ST s (STArray s Int Double)
forM_ [1 .. end] $ \i -> do
let cval = vals !! i
let lval = vals !! (i-1)
writeArray myArray i ((cval + lval)/2)
getElems myArray