題目描述:
給定一個數組和滑動窗口的大小,找出所有滑動窗口里數值的最大值。例如,如果輸入數組{2,3,4,2,6,2,5,1}及滑動窗口的大小3,那么一共存在6個滑動窗口,他們的最大值分別為{4,4,6,6,6,5}; 針對數組{2,3,4,2,6,2,5,1}的滑動窗口有以下6個: {[2,3,4],2,6,2,5,1}, {2,[3,4,2],6,2,5,1}, {2,3,[4,2,6],2,5,1}, {2,3,4,[2,6,2],5,1}, {2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。
解題思路:
如果不考慮時間開銷,使用蠻力法,本題並不難解決,依次遍歷所有的滑動窗口,掃描每個窗口中的所有數字並找出其中的最大值,這都很容易實現,但是如果滑動窗口的大小為k,那么需要O(k)的時間找最大值,對於長度為n大的數組,總的時間復雜度為O(nk)。
然后我們考慮進一步優化,一個滑動窗口實際上可以看成一個隊列。當窗口滑動時,處於窗口第一個位置的數字被刪除,同時在窗口的末尾又增加了一個新的數字。這符合隊列“先進先出”的特性。
在第20題:包含min函數的棧,我們使用兩個棧實現了一個求最小值的棧,在O(1)時間內可以得到最小值,這里我們可以改為求最大值,同樣可以在O(1)時間內得到最大值,而這里的數據用隊列保存,我們可以用兩個棧實現隊列,這就是第5題:用兩個棧實現隊列。這樣,實際上綜合這兩題我們可以解決本題,總的時間復雜度也可以降到O(n)。
這里我們換用另外一種方法:使用雙端隊列。我們不把所有的值都加入滑動窗口,而是只把有可能成為最大值的數加入滑動窗口。這就需要一個兩邊都可以操作的雙向隊列。
我們以數組{2,3,4,2,6,2,5,1}為例,滑動窗口大小為3,先把第一個數字2加入隊列,第二個數字是3,比2大,所以2不可能是最大值,所以把2刪除,3存入隊列。第三個數是4,比3大,同樣刪3存4,此時滑動窗口以遍歷三個數字,最大值4在隊列的頭部。
第4個數字是2,比隊列中的數字4小,當4滑出去以后,2還是有可能成為最大值的,因此將2加入隊列尾部,此時最大值4仍在隊列的頭部。
第五個數字是6,隊列的數字4和2都比它小,所以刪掉4和2,將6存入隊列尾部,此時最大值6在隊列頭部。
第六個數字是2,此時隊列中的數6比2大,所以2以后還有可能是最大值,所以加入隊列尾部,此時最大值6在仍然隊列頭部。
······
依次進行,這樣每次的最大值都在隊列頭部。
還有一點需要注意的是:如果后面的數字都比前面的小,那么加入到隊列中的數可能超過窗口大小,這時需要判斷滑動窗口是否包含隊頭的這個元素,為了進行這個檢查,我們可以在隊列中存儲數字在數組中的下標,而不是數值,當一個數字的下標和當前出來的數字下標之差大於等於滑動窗口的大小時,這個元素就應該從隊列中刪除。
舉例:
編程實現(Java):
import java.util.*;
public class Solution {
public ArrayList<Integer> maxInWindows(int [] num, int size){
/*
思路:用雙端隊列實現
*/
ArrayList<Integer> res=new ArrayList<>();
if(num==null || num.length<1 || size<=0 || size>num.length)
return res;
Deque<Integer> queue=new LinkedList<>();
for(int i=0;i<num.length;i++){
while(!queue.isEmpty() && queue.peek()<i-size+1) //超出范圍的去掉
queue.poll();
//當前值大於之前的值,之前的不可能是最大值,可以刪掉
while(!queue.isEmpty() && num[i]>=num[queue.getLast()])
queue.removeLast();
queue.add(i);
if(i>=size-1){ //此時開始是第一個滑動窗口
res.add(num[queue.peek()]);
}
}
return res;
}
}