在 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 这正是sequenceA
在ZipList
s 上所做的事情。 :)(“或”我的意思是“或者它可以明确写成......”)。 sequenceA
== foldr ((<*>).((:)<$>)) (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 所说,如果您想使用整个结果,时间复杂度并不会更好。但是,如果您只使用某些窗口,我们现在可以避免在您不使用的窗口上执行 take
和 length
的工作。
【讨论】:
【参考方案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 中实现“鼠标滑到超链接上时,出现一个下拉列表”,注意,不是层做的