給出一個序列,要求找出滑動視窗中的最大值,比如:
# 序列: 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) 的佇列。
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_out
與 stack_in
棧頂的最大值相比較即可得到第二個滑動視窗的最大值。也就是說當一個滑動視窗的元素被分散在兩個棧中時,我們需要 O(1) + O(1) + O(1) = O(1)
的時間找到該滑動視窗的最大值。三個 O(1)
依次為:從 stack_out
找到第一部分最大值的時間、從 stack_in
中找到另一部分最大值的時間、比較兩個最大值得到最終的最大值的時間。
依次處理下去,便可得到我們想要的結果。
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))