繁体   English   中英

O(n) 时间内的滑动窗口最大值

[英]Sliding window maximum in O(n) time

输入:

listi = [9, 7, 8, 4, 6, 1, 3, 2, 5]

输出:

# m=3
listo = [9, 8, 8, 6, 6, 3, 5]

给定一个由n数字组成的随机列表,我需要找到m连续元素的所有子列表,从子列表中选择最大值并将它们放入一个新列表中。

def convert(listi, m):
    listo = []
    n = len(listi)
    for i in range(n-m+1):
        listo.append(max(listi[i:3+i]))
    return listo

这个实现的时间复杂度是O(m\\^{(n-m+1)} ,如果listi很长,这很糟糕,有没有办法在O(n)的复杂度中实现它?

令人惊讶的是,这个算法的易于访问的描述并不那么容易理解,所以诀窍是这样的:

当您在长度为n的列表上滑动长度为m的窗口时,您会维护当前窗口中所有元素的双端队列,该双端队列可能在某些时候成为任何窗口中的最大值。

如果当前窗口中的元素大于窗口中出现在它之后的所有元素,则它可能会成为最大值。 请注意,这始终包括当前窗口中的最后一个元素。

由于双端队列中的每个元素都 > 其后的所有元素,因此双端队列中的元素是单调递减的,因此第一个是当前窗口中的最大元素。

当窗口向右滑动一个位置时,您可以按如下方式维护此双端队列:从末尾删除 <= 新元素的所有元素。 然后,将新元素添加到双端队列的末尾。 如果从窗口前端掉落的元素是双端队列中的第一个元素,则将其删除。 由于每个元素最多添加和删除一次,因此维护此双端队列所需的总时间为 O(n)。

为了便于判断位于双端队列前面的元素何时掉出窗口,请将元素的索引而不是它们的值存储在双端队列中。

这是一个相当有效的python实现:

def windowMax(listi, m):
    # the part of this list at positions >= qs is a deque
    # with elements monotonically decreasing.  Each one
    # may be the max in a window at some point
    q = []
    qs = 0

    listo=[]
    for i in range(len(listi)):

        # remove items from the end of the q that are <= the new one
        while len(q) > qs and listi[q[-1]] <= listi[i]:
            del q[-1]

        # add new item
        q.append(i)

        if i >= m-1:
            listo.append(listi[q[qs]])
            # element falls off start of window
            if i-q[qs] >= m-1:
                qs+=1

        # don't waste storage in q. This doesn't change the deque
        if qs > m:
            del q[0:m]
            qs -= m
    return listo

有一个漂亮的解决方案,它的运行时间与 M 无关。

在下图中,第一行表示初始序列。在第二行中,我们有从左到右的 1, 2, ... M 个连续元素组的最大值(“前缀”最大值)。 在第三行,我们有 1, 2, ... M 个连续元素组的最大值,从右到左(“后缀”最大值)。 在第四行,第二行和第三行元素的最大值。

a   b   c    d    e    f    g    h    i    j    k    l    m    n    o

a   ab  abc  d    de   def  g    gh   ghi  j    jk   jkl  m    mn   mno
        abc  bc   c    def  ef   f    ghi  hi   i    jkl  kl   l    mno  no   o

        abc  bcd  cde  def  efg  fgh  ghi  hij  ijk  jkl  klm  lmn  mno          

请注意,第三行中有重复的元素,我们不需要计算它们。

第二行的计算对每片 M 个元素进行 M-1 次比较; 第二行 M-2 和第三行 M。因此忽略末尾的影响,我们对每个元素执行略少于 3 次的比较。

所需的存储是一个额外的 M 个元素数组,用于临时评估第三行的切片。

我尝试使用zip计时,结果似乎比您当前的功能快 50% - 虽然无法真正说出时间复杂度的差异。

import timeit

setup = """
from random import randint
listi = [randint(1,100) for _ in range(1000)]

def convert(iterable, m):
    t = [iterable[x:] for x in range(m)]
    result = [max(combo) for combo in zip(*t)]
    return result"""

print (min(timeit.Timer('a=listi; convert(a,3)', setup=setup).repeat(7, 1000)))
#0.250054761


setup2 = """
from random import randint
listi = [randint(1,100) for _ in range(1000)]

def convert2(listi, m):
    listo = []
    n = len(listi)
    for i in range(n-m+1):
        listo.append(max(listi[i:3+i]))
    return listo"""

print (min(timeit.Timer('a=listi; convert2(a,3)', setup=setup2).repeat(7, 1000)))
#0.400374625

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM