滑動視窗最大值問題

呆呆熊大人發表於2022-07-17

前言:滑動視窗最大值問題是很經典的演算法問題。本文描述了它的求解過程,分析了時間複雜度,證明了其正確性。

什麼是滑動視窗最大值問題

用一個長度固定的滑動視窗W在一個陣列上逐元素滑動,求每次滑動後W內的最大元素。例如:長度為3的滑動視窗在陣列[3,4,1,5,2]上滑動,在(a)和(b)兩個時刻視窗內最大值分別為4和5。如下圖所示:

你可以在leetcode上找到相關題目。

最容易想到的方法就是遍歷每一時刻下滑動視窗內的所有元素,找到其最大值。整個過程簡單直觀,無需證明。複雜度上,易得空間複雜度是O(1),時間複雜度是O(N*K),其中N是陣列長度,K是滑動視窗大小。因為需要滑動N次,每次遍歷K個元素。

高階解法

高階解法在整個滑動過程中維護了一個佇列Q。某時刻滑窗滑入的新元素記為x、滑出的舊元素記為z,則Q的更新步驟為:

  • 更新步驟一: 從Q的尾部依次彈出所有小於x的元素,然後把x壓入Q的隊尾
  • 更新步驟二: 如果Q的隊首元素等於z,則Q彈出隊首

執行以上操作後,Q的隊首元素即為當前滑動視窗最大值。過程如圖所示:

複雜度分析

空間上,需要額外的空間儲存Q,Q的最大長度不超過K,因此空間複雜度是O(K)。

時間上,共迭代了N次,每次需要對Q做壓入和彈出操作。最差的情況,某次迭代會彈出Q所有元素。Q的長度最大為K,所以貌似時間代價是O(NK);但實際上,從整體看,所有的元素都會壓入一次,彈出一次,所以實際複雜度其實為O(N)。

證明

演算法每次迭代都維持了一個條件不變式:

  • 條件一:Q是W的子序列。

  • 條件二:在W中,\(q_0\)是最大的元素,\(q_{i+1}\)\(q_{i}\)之後最大的元素。陣列下標從0開始。

例如上圖中,

  • Q=(8,6,4)是W的子序列
  • 在W中,8是W中最大的元素,6是8之後最大的元素,4是6之後最大的元素

值得注意的是,如果最大的元素存在多個,則認為是它們中的第一個。

過程分析

按照上文的更新規則,更新過程可以表述為:

  1. 滑窗納入新元素x,x擠掉Q尾部所有小於它的元素,然後追加到Q。
  2. 滑窗彈出舊元素z,如果Q首元素等於z,則從Q彈出首元素

我們先來關注過程一。過程一可能有幾個情況:

Case 1: x擠掉了Q的所有元素,說明它大於Q中所有元素,此時Q中只剩下x,顯然這時候滿足條件二。

Case 2: x沒有擠掉任何Q的元素,說明x小於等於Q的所有元素。此時x會追加到Q的尾部,顯然這時候滿足條件二。

Case 3:x擠掉了Q的一部分元素,說明x大於Q尾部的一個或多個元素。

我們假設

\[q_k \ge x > \{ q_{k+1},...,q_m \} \]

則x會擠掉原來的 \(\{ q_{k+1},...,q_m \}\) 成為新的\(q_{k+1}\)

此時條件二成立:x比\(q_0\)小,不會動搖它的地位;x比\(q_{k+1}\)大,而後者是W中\(q_k\)之後最大的元素,因此x自然也是\(q_k\)之後最大的元素,那麼x成為新的\(q_{k+1}\)後,自然滿足條件二。

此時我們關注過程一。如果滑窗彈出的元素就是\(q_0\),那麼Q自然也應該彈出\(q_0\),這樣才能滿足條件一。

  • 如果W彈出的元素z不等於\(q_0\),那麼無需處理,如下圖中情況(a)所示。

  • 如果z等於\(q_0\),那麼可以確定彈出的就是\(q_0\),畢竟\(q_0\)是W中的最大元素(如果最大元素有多個則為第一個)。此時W和Q都彈出\(q_0\)後,依然符合條件一和條件二,這裡不再證明。情況如下圖(b)所示。

至此,證明完畢。

我們可以簡單理解一下:Q是W的子序列,當W失去元素z時,Q自然也要失去它;如果W新來了一個很大的元素x,那麼直到它被彈出之前,在前面比它小的元素是沒有出頭之日的。在它彈出之後,它前面的元素自然也早被彈出了。所以在前面比它小的元素都沒有用,可以即刻彈出。

相關文章