在Haskell中实现高效的滑动窗口算法

时间:2014-12-31 22:06:08

标签: haskell time-complexity sliding-window

我在Haskell中需要一个高效的滑动窗口函数,所以我写了以下内容:

windows n xz@(x:xs)
  | length v < n = []
  | otherwise = v : windows n xs
  where
    v = take n xz

我的问题是我认为复杂度是O(n * m),其中m是列表的长度,n是窗口大小。您将take的列表倒计时一次,length的另一次倒计时,然后在基本上为m-n次的列表中进行倒计时。看起来它可能比这更有效,但我对如何使它更加线性感到茫然。任何人?

5 个答案:

答案 0 :(得分:6)

您可以使用Seq中的Data.Sequence,其中有O(1)入队并在两端出队:

import Data.Foldable (toList)
import qualified Data.Sequence as Seq
import Data.Sequence ((|>))

windows :: Int -> [a] -> [[a]]
windows n0 = go 0 Seq.empty
  where
    go n s (a:as) | n' <  n0   =              go n' s'  as
                  | n' == n0   = toList s'  : go n' s'  as
                  | otherwise =  toList s'' : go n  s'' as
      where
        n'  = n + 1         -- O(1)
        s'  = s |> a        -- O(1)
        s'' = Seq.drop 1 s' -- O(1)
    go _ _ [] = []

请注意,如果您实现整个结果,则算法必须为O(N * M),因为这是结果的大小。使用Seq只会通过常数因素提高性能。

使用示例:

>>> windows [1..5]
[[1,2,3],[2,3,4],[3,4,5]]

答案 1 :(得分:6)

你不能比 O(m * n)更好,因为这是输出数据结构的大小。

但是,如果颠倒操作顺序,则可以避免检查窗口的长度:首先创建 n 移位列表,然后将它们压缩在一起。压缩将摆脱那些没有足够元素的东西。

import Control.Applicative
import Data.Traversable (sequenceA)
import Data.List (tails)

transpose' :: [[a]] -> [[a]]
transpose' = getZipList . sequenceA . map ZipList

压缩列表列表只是transposition,但与transpose中的Data.List不同,它会丢弃少于 n 元素的输出。< / p>

现在可以轻松实现窗口功能:取 m 列表,每个列表移1,然后压缩它们:

windows :: Int -> [a] -> [[a]]
windows m = transpose' . take m . tails

也适用于无限列表。

答案 2 :(得分:4)

首先让我们在不担心最后的短片的情况下获取窗口:

import Data.List (tails)

windows' :: Int -> [a] -> [[a]]
windows' n = map (take n) . tails

> windows' 3 [1..5]
[[1,2,3],[2,3,4],[3,4,5],[4,5],[5],[]]

现在我们想要在不检查每个短片的情况下摆脱短片。

既然我们知道它们已经结束了,我们可能会失败它们:

windows n xs = take (length xs - n + 1) (windows' n xs)

但这并不是很好,因为我们仍需要花费额外的时间来获得它的长度。它也不适用于原始解决方案所做的无限列表。

相反,让我们编写一个函数,使用一个列表作为标尺来衡量从另一个列表中获取的金额:

takeLengthOf :: [a] -> [b] -> [b]
takeLengthOf = zipWith (flip const)

> takeLengthOf ["elements", "get", "ignored"] [1..10]
[1,2,3]

现在我们可以写下这个:

windows :: Int -> [a] -> [[a]]
windows n xs = takeLengthOf (drop (n-1) xs) (windows' n xs)

> windows 3 [1..5]
[[1,2,3],[2,3,4],[3,4,5]]

也适用于无限列表:

> take 5 (windows 3 [1..])
[[1,2,3],[2,3,4],[3,4,5],[4,5,6],[5,6,7]]

正如Gabriel Gonzalez所说,如果你想要使用整个结果,时间复杂性并不是更好。但是,如果您只使用某些窗口,我们现在设法避免对您不使用的窗口执行takelength的工作。

答案 3 :(得分:2)

如果你想要O(1)长度那么为什么不使用提供O(1)长度的结构呢?假设您没有从无限列表中查找窗口,请考虑使用:

import qualified Data.Vector as V
import Data.Vector (Vector)
import Data.List(unfoldr) 

windows :: Int -> [a] -> [[a]]
windows n = map V.toList . unfoldr go . V.fromList
 where                    
  go xs | V.length xs < n = Nothing
        | otherwise =
            let (a,b) = V.splitAt n xs
            in Just (a,b)

每个窗口从向量到列表的对话可能会让你感到厌烦,我不会在那里冒出一个乐观的猜测,但我敢打赌,性能优于仅列表版本。

答案 4 :(得分:0)

对于滑动窗口,我还使用了未装箱的Vetors作为长度,take,drop以及splitAt是O(1)操作。

来自Thomas M. DuBuisson的代码是一个移动的窗口,而不是滑动,除非n = 1。因此缺少(++),但是其成本为O(n + m)。因此小心,你把它放在哪里。

 import qualified Data.Vector.Unboxed as V
 import Data.Vector.Unboxed (Vector)
 import Data.List

 windows :: Int -> Vector Double -> [[Int]]
 windows n = (unfoldr go) 
  where                    
   go !xs | V.length xs < n = Nothing
          | otherwise =
             let (a,b) = V.splitAt 1 xs
                  c= (V.toList a ++V.toList (V.take (n-1) b))
             in (c,b)

我用+RTS -sstderr

尝试了
 putStrLn $ show (L.sum $ L.concat $  windows 10 (U.fromList $ [1..1000000]))

并获得实时1.051s和96.9%的使用率,请记住在滑动窗口后执行两次O(m)操作。