zoukankan      html  css  js  c++  java
  • 滑动窗口的最大值问题

    给出一个序列,要求找出滑动窗口中的最大值,比如:

    # 序列: 2, 6, 1, 5, 3, 9, 7, 4
    # 窗口大小: 4
    
    [2,  6,  1,  5], 3,  9,  7,  4    => 6
     2, [6,  1,  5,  3], 9,  7,  4    => 6
     2,  6, [1,  5,  3,  9], 7,  4    => 9
     2,  6,  1, [5,  3,  9,  7], 4    => 9
     2,  6,  1,  5, [3,  9,  7,  4]   => 9
    
    # 期望结果: [6, 6, 9, 9, 9]
    

    并要求算法的时间复杂度为 O(n)

    算法思路

    稍加观察便能发现滑动窗口其实就是一个队列:窗口每滑动一次,相当于出列一个元素,并入列一个元素。因此这个问题实际上也可以看作是要求设计一个 pop(), push(), max() 均为 O(1) 的队列。如果能设计出这样的队列类型,那么在窗口滑动的过程中不断地入列出列,则最终只需要 O(n) 的时间便能找到所有滑动窗口的最大值。

    pop()push() 做到 O(1) 很简单,max() 就没那么容易了。随着元素的进队,我们可以记录元素之间的大小关系,维护一个最大值记录,但当队首元素弹出时,已有的大小关系就会被破坏——被弹出的元素可能就是最大值,这样就需要重新开始评估新的最大值。但若我们只从队末弹出呢?这样便不会破坏已记录的剩余元素的最大值。这种只在一端进出的数据结构就是。只要在进栈的同时维护一个最大值栈,我们就可以轻松得到一个 pop(), push(), max() 均为 O(1) 的栈。比如令 2, 7, 4 依次进栈,并同时维护一个当前时刻的最大值栈 2, 7, 7,弹出一个元素的时候也同时弹出最大值栈中的元素,这样我们就可以在 O(1) 的时间内找到一个栈的 max

    我们知道,使用两个栈可以构造一个队列,即一个栈用于 push,一个栈用于 pop,因此我们可以使用两个 max()O(1) 操作的栈来构造一个 max()O(1) 的队列。

    这是因为一个滑动窗口中的元素要么全在一个栈中,此种情况下只需 O(1) 的时间便可得到该滑动窗口的最大值;要么一部分在一个栈中,一部分在另一个栈中,而从两个栈中找到各自的最大值只需要 O(1) 的时间,再比较两个部分各自的最大值,便可以得到该滑动窗口的最大值,因此此种情况下也只需要 O(1) 的操作就可以得到该滑动窗口的最大值。

    以序列 2, 6, 1, 5, 3, 9, 7, 4 为例,设其滑动窗口的大小为 4,记用于出列的栈为 stack_out,用于入列的栈为 stack_in。首先得到第一个滑动窗口,即入列 4 个元素:

        stack_out:		stack_in:
                             (5, 6)         <- Top
           None              (1, 6)
                             (6, 6)
                             (2, 2)         <- Bottom
    

    使用 (value, max) 表示当前要入栈的元素 value 以及当前的最大值 max。此时只需要读出 stack_in 栈顶元素的最大值即为当前滑动窗口的最大值。

    向右滑动一格即表示将 2 出列,将 3 入列:

    stack_out:		stack_in:
      		  	                    <- Top
      (6, 6)	
      (1, 5)		
      (5, 5)		  (3, 3)	    <- Bottom
    

    此时便得到了第二个滑动窗口。它的元素被分置在两个栈中:有 3 个元素在 stack_out 中、 1 个元素在 stack_in 中。而我们可以用 O(1) 的时间从 stack_out 中找到 3 个元素这个部分中的最大值,同时用 O(1) 的时间从 stack_in 中找到另一部分的最大值。因此将 stack_outstack_in 栈顶的最大值相比较即可得到第二个滑动窗口的最大值。也就是说当一个滑动窗口的元素被分散在两个栈中时,我们需要 O(1) + O(1) + O(1) = O(1) 的时间找到该滑动窗口的最大值。三个 O(1) 依次为:从 stack_out 找到第一部分最大值的时间、从 stack_in 中找到另一部分最大值的时间、比较两个最大值得到最终的最大值的时间。

    依次处理下去,便可得到我们想要的结果。

    时间复杂度

    我们在前文提到「设计一个 pop(), push(), max() 均为 O(1) 的队列」。但似乎上述算法中 pop() 的时间复杂度不是 O(1),因为使用两个栈实现的队列类型在出列时会遇到以下两种情形:

    1. 当用于出列的栈 stack_out 不空时,可以直接弹出 stack_out 的栈顶元素以出列;
    2. 当用于出列的栈 stack_out 为空时,我们首先需要将用于入列的栈 stack_in 中的元素全部转移到 stack_out 中,然后再弹出其栈顶元素以出列。

    显然情形 1) 是 O(1) 的,但是情形 2) 是 O(m) 的(记 m 为每个栈的大小,也即滑动窗口的大小)。如果我们所设计的队列的 pop() 可能不是 O(1) 的,那么算法的最终时间复杂度还会是 O(n) 吗?

    我们注意到只有当 stack_out 为空(即情形 2)时,才会需要 O(m) 的时间出列一个元素。因此若将算法的最终时间复杂度记为 O(nm),虽然正确,但这样做未免太过悲观,毕竟情形 2 出现的次数相对较少,而其它时间又都是 O(1) 的。所以我们希望可以得到一个更小的时间复杂度上界。一个比较简单的想法是,在遇到情形 2 之前,我们首先需要做 m 次出栈操作将 stack_out 清空,然后才会发生情形 2,也就是将 stack_in 中的 m 个元素移入到 stack_out 中,这一步操作就是我们的“罪魁祸首”,但我们可以把这个“罪名”分别安到在此之前的 m 次用时为 O(1) 的出栈操作上,即将它之前的 m 次出栈操作看作是 O(2) 的,如此一来就除掉了这个「罪魁祸首」,代价则是所有的出栈操作都变成了 O(2),不过好在也是 O(1) 的。因此我们的 pop() 操作的时间复杂度还是 O(1) 的,算法最终的时间复杂度为 O(n)。这种时间复杂度也被称为均摊时间复杂度——我们将一个代价比较高的操作转移到其它操作上,从而降低所有操作的平均时间复杂度。这种平均时间复杂度考虑的是一系列操作的平均时间复杂度,而不是单个操作的时间复杂度。

    当然我们也可以从另外一个角度来考虑该算法的时间复杂度:即考虑每个元素进栈出栈的次数。我们使用了两个栈来实现了这个队列,从算法开始到结束,每个元素分别先从 stack_in 进栈、出栈,然后再从 stack_out 进栈、出栈,而这些操作都是 O(1) 的,因此我们最终只需要 O(4n) 的时间来完成所有操作,因此该算法的时间复杂度为 O(n)

    P.S.
    如果在实现上有疑惑,不妨看看下面给出的这种队列类型的 Python 代码。在该代码中,入队操作被命名为 append,而不是 push,其目的是与 Python 标准库中队列的方法名保持一致。

    from typing import List
    from collections import namedtuple
    
    Node = namedtuple('Node', ['value', 'max'])
    
    
    class MaxQueue():
    
        def __init__(self, stack_len: int) -> None:
            self.stack_in = []
            self.stack_out = []
            self.stack_len = stack_len
    
        def pop(self) -> int:
            if not self.stack_out:
                if not self.stack_in:
                    raise IndexError('pop from an empty queue')
                else:
                    self._move_in_to_out()
            return self.stack_out.pop().value
    
        def append(self, value: int) -> None:
            if len(self.stack_in) >= self.stack_len:
                if self.stack_out:
                    raise IndexError('the queue is full')
                else:
                    self._move_in_to_out()
            self._push_to_stack(self.stack_in, value)
    
        def max(self) -> int:
            if self.stack_in and self.stack_out:
                return max(self.stack_in[-1].max, self.stack_out[-1].max)
            if self.stack_in:
                return self.stack_in[-1].max
            if self.stack_out:
                return self.stack_out[-1].max
    
        def _move_in_to_out(self) -> None:
            while self.stack_in:
                self._push_to_stack(self.stack_out,
                                    self.stack_in.pop().value)
    
        def _push_to_stack(self, stack: List[Node], value: int) -> None:
            if stack:
                stack.append(Node(value, max=max(value, stack[-1].max)))
            else:
                stack.append(Node(value, max=value))
    
    
  • 相关阅读:
    ArrayList用法
    MessageBox
    将文本文件导入Sql数据库
    在桌面和菜单中添加快捷方式
    泡沫排序
    Making use of localized variables in javascript.
    Remove double empty lines in Visual Studio 2012
    Using Operations Manager Connectors
    Clear SharePoint Designer cache
    Programmatically set navigation settings in SharePoint 2013
  • 原文地址:https://www.cnblogs.com/andywenzhi/p/11275990.html
Copyright © 2011-2022 走看看