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

Posted

技术标签:

【中文标题】O(n) 时间内的滑动窗口最大值【英文标题】:Sliding window maximum in O(n) time 【发布时间】:2019-12-04 08:32:01 【问题描述】:

输入:

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)的复杂度中实现这个?

【问题讨论】:

我认为你可以,稍微牺牲一下空间复杂度——这应该不是问题。我会试一试。顺便问一下,O(m\^(n-m+1) 是什么?你能编辑一下吗? 为什么8和6在结果列表中出现了两次? @John - 因为 7-8-4 和 8-4-6 的最大值均为 8。类似地,4-6-1 和 6-1-3 的最大值为 6。 啊——我没有将“连续”解释为“在列表中相邻”。 方法三:geeksforgeeks.org/… 【参考方案1】:

我尝试使用 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

【讨论】:

【参考方案2】:

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

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

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

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

当窗口向右滑动一个位置时,您可以按如下方式维护此双端队列:从末尾删除所有

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

这是一个相当有效的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

【讨论】:

【参考方案3】:

有一个运行时间独立于 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 元素数组,用于临时评估第三行的切片。

来源:高效膨胀、侵蚀、开闭 算法, JOSEPH (YOSSI) GIL & RON KIMMEL。

【讨论】:

这很好,现在我已经弄清楚了。我会这样说:考虑位置 kM-1 的元素。称它们为“标记” 每个窗口都包含一个标记。任何窗口中的最大值是从第一个元素到标记的最大值与从标记到最后一个元素的最大值中的较大者。如果您为每个元素计算前一个标记的最大值和下一个标记的最大值,那么您可以在恒定时间内获得任何窗口中的最大值。如果我希望有一天我能找到这种技术的用途。

以上是关于O(n) 时间内的滑动窗口最大值的主要内容,如果未能解决你的问题,请参考以下文章

79. 滑动窗口的最大值

华为机试真题 Java 实现滑动窗口最大值

滑动窗口

8.最大滑动窗口问题

239-滑动窗口最大值

华为OD机试 -滑动窗口最大和(Java) | 机试题+算法思路+考点+代码解析 2023