[英]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.