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

Posted

技术标签:

【中文标题】在 Haskell 中实现一个高效的滑动窗口算法【英文标题】:Implementing an efficient sliding-window algorithm in Haskell 【发布时间】:2015-02-27 20:59:37 【问题描述】:

在 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 次的列表中倒计时。似乎它可以比这更有效,但我不知道如何使它更线性。有接盘侠吗?

【问题讨论】:

【参考方案1】:

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

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

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

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

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

现在很容易制作窗口函数:获取 m 个列表,每个列表移动 1,然后压缩它们:

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

也适用于无限列表。

【讨论】:

foldr (zipWith (:)) (repeat []) . take m . tails @Will Ness - 哦,太好了 @user1441998 这正是sequenceAZipLists 上所做的事情。 :)(“或”我的意思是“或者它可以明确写成......”)。 sequenceA == foldr ((&lt;*&gt;).((:)&lt;$&gt;)) (pure []). @Will Ness -- 感谢您的澄清,更好!【参考方案2】:

您可以使用 Data.Sequence 中的 Seq,它的两端有 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]]

【讨论】:

Data.Sequence 太过分了,IMO。你只需要一个队列!我想,像银行家队列这样轻量级的东西应该可以很好地解决问题。【参考方案3】:

首先让我们得到窗口,而不用担心最后的短窗口:

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)

但这并不是很好,因为我们仍然要通过 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]]

正如 Gabriella Gonzalez 所说,如果您想使用整个结果,时间复杂度并不会更好。但是,如果您只使用某些窗口,我们现在可以避免在您不使用的窗口上执行 takelength 的工作。

【讨论】:

【参考方案4】:

如果你想要 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)

每个窗口从向量到列表的对话可能会让您有些吃惊,我不会冒险乐观地猜测,但我敢打赌,性能比仅列表版本更好。

【讨论】:

我敢打赌这是低效的。流融合不会使您免于实现向量(通过数组加倍)。像 David Fletcher 或(我认为)Petr Pudlák 的解决方案看起来更有希望。 Gabriel Gonzalez 的速度可能会比所写的要慢,但使用更便宜的队列最终可能会更快一些(基于重新实现 inits 的经验的怀疑)。 这不是 Q 所要求的;它应该类似于最后一行的Just (a, V.tail xs)。或者直接使用slice 而不使用splitAt。然后建议将切片保持原样,而不将它们转换为列表,用于线性空间......【参考方案5】:

对于滑动窗口,我还使用了未装箱的 Vetor,因为长度、取值、丢弃以及 splitAt 都是 O(1) 操作。

Thomas M. DuBuisson 的代码是一个 n 移动窗口,而不是滑动窗口,除非 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) 操作。

【讨论】:

以上是关于在 Haskell 中实现一个高效的滑动窗口算法的主要内容,如果未能解决你的问题,请参考以下文章

算法--滑动窗口

精读《算法 - 滑动窗口》

如何在Macromedia Dreamweaver 8 中实现“鼠标滑到超链接上时,出现一个下拉列表”,注意,不是层做的

栈和队列----算法

如何在 Haskell 中实现 B+ 树?

在 Haskell 中实现 Smullyan 的算术鸟