我在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次的列表中进行倒计时。看起来它可能比这更有效,但我对如何使它更加线性感到茫然。任何人?
答案 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所说,如果你想要使用整个结果,时间复杂性并不是更好。但是,如果您只使用某些窗口,我们现在设法避免对您不使用的窗口执行take
和length
的工作。
答案 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)操作。