【劍指offer】59 - I. 滑動窗口的最大值

劍指 Offer 59 - I. 滑動窗口的最大值

知識點:隊列;滑動窗口;單調數組

題目描述

給定一個數組 nums 和滑動窗口的大小 k,請找出全部滑動窗口裏的最大值。ui

示例
輸入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
輸出: [3,3,5,5,6,7] 
解釋: 

  滑動窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

解法一:滑動窗口+雙端隊列+單調

滑動窗口整體上分紅兩類,一類是可變長度的滑動窗口,一類是固定長度的滑動窗口,這道題目就是固定長度的。在遍歷元素時,爲了保持窗口的大小固定,右側元素進入窗口後,左側元素要可以出去。而後直到遍歷結束。
想一下剛纔的過程,右側元素進入,左側元素出去,這不就是雙端隊列嗎?因此這道題目能夠藉助雙端隊列來解;code

想一下咱們常常會遇到求一個隊列或者一個窗口一個棧內的最大最小值,怎麼求呢,最簡單的方法就是遍歷這個窗口這個棧,這樣時間複雜度就是O(N),有沒有辦法能在O(1)時間內得到一個棧或者一個窗口內的最值呢,這其實就是劍指offer30題,好比獲取一個棧內的最小值,咱們能夠採用一個輔助棧,這個輔助棧有一個最大的特色就是單調的,也就是咱們俗稱的單調棧。好比咱們維持一個單調遞減棧,若是當前值比棧頂元素大,那就不要了,由於咱們最後只獲取最小值,若是比當前棧頂小,那就入棧,也就是更新了最小值;這樣就能夠在O(1)的時間內得到棧內最小值了,由於最小值就是輔助棧的棧頂。索引

這道題目也相似啊,咱們須要得到窗口內的最大值,這不就是一個雙端隊列的最大值嗎,因此咱們要維持一個單調遞減的雙端隊列,如何實現呢,每次入隊前,判斷此值與隊尾元素的大小,小於的話就入隊,這樣就維持了一個單調遞減隊列;若是元素比隊尾值要大,那就要將隊尾元素出隊了,由於咱們只關注大的值,可不能把這個大值錯過了,這裏面的小值就不用管了。隊列

好比[5,3,4], 4要入隊的時候發現3比其小,因此3從隊尾出去,4入隊;leetcode

class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int len = nums.length;
        if(len == 0) return nums;
        int[] res = new int[len-k+1];
        Deque<Integer> deque = new LinkedList<>(); //雙端隊列;
        int index = 0;
        //未造成窗口;
        for(int i = 0; i < k; i++){
            while(!deque.isEmpty() && nums[i] > deque.peekLast()){
                deque.removeLast(); //保證單調遞減隊列;
            }
            deque.offerLast(nums[i]);
        }
        res[index++] = deque.peekFirst();  //隊首始終是最大的;
        //滑動窗口;
        for(int i = k; i < len; i++){ //i表明當前窗口最後一個元素的索引;
            //保證隊內只含有窗口內的元素,因此當窗口的前一個元素等於隊首的時候,要將隊首出隊;
            if(deque.peekFirst() == nums[i-k]){
                deque.removeFirst(); 
            }
            while(!deque.isEmpty() && nums[i] > deque.peekLast()){
                deque.removeLast();  //保證單調遞減隊列;
            }
            deque.offerLast(nums[i]);
            res[index++] = deque.peekFirst(); //隊首始終是當前窗口內最大的;
        }
        return res;
    }
}

解法二:滑動窗口+單調

上面的作法咱們每次入隊的是元素的值,本質上就是用雙端隊列來模擬了窗口的滑動,雙端隊列是單調隊列;
其實咱們也能夠用一個單調隊列,入隊的是元素的下標索引。這樣其實咱們能很明顯的看出窗口的滑動,只要隊首元素的下標<窗口的左邊界,那就要把隊首移除了,窗口進行了一次滑動;一個很明顯的不一樣,入隊的是下標索引rem

流程get

  • 遍歷給定數組中的元素,若是隊列不爲空且當前考察元素大於等於隊尾元素,則將隊尾元素移除。直到,隊列爲空或當前考察元素小於新的隊尾元素;
  • 當隊首元素的下標小於滑動窗口左側邊界left時,表示隊首元素已經再也不滑動窗口內,所以將其從隊首移除。
  • 因爲數組下標從0開始,所以當窗口右邊界right+1大於等於窗口大小k時,意味着窗口造成。此時,隊首元素就是該窗口內的最大值。
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        int[] res = new int[nums.length-k+1];
        if(nums.length == 0 || nums == null) return new int[]{}; //特例爲空;
        Deque<Integer> deque = new LinkedList<>();
        //right 爲窗口右邊界;
        for(int right = 0; right < nums.length; right++){
            //若是隊列不爲空且當前考察值>隊尾元素,將隊尾元素移除,直到爲空或遇到大的;
            while(!deque.isEmpty() && nums[right] > nums[deque.peekLast()]){
                deque.removeLast();
            }
            deque.offerLast(right); //存儲下標;
            int left = right-k+1; //窗口左側邊界下標;
            if(deque.peekFirst() < left){
                deque.removeFirst();  //窗口進行了移動,左側出去;
            }
            if(right + 1 >= k){
                res[left] = nums[deque.peekFirst()]; //這時候窗口造成,開始逐步獲得答案;
            }
        }
        return res;
    }
}

體會

  • 滑動窗口一共有兩種類型:io

    • 窗口長度可變:這種類型中長度是能夠變化的,一個基本的流程就是,右邊界長,而後到達某一個條件(好比窗口內的和達到某個值,窗口中出現了重複的元素),這時候右邊界停下來,左邊界長,而後跳出這個條件(好比窗口內的和又小於目標值了,好比窗口中又沒有重複元素了),這時候右邊界再去移動;(咱們要處理的始終保證窗口內知足某個條件,例如窗口內的值小於某值,窗口內沒有重複的,只要不知足了就去移左邊界);
    • 窗口長度固定,好比說固定一個長度的窗口的時候,那右邊界長的時候,左邊界也得跟着長,維持一個窗口的恆定值;
  • 要始終明白滑動窗口的左右邊界是不會出現回退的,兩個邊界確定都是朝着一個方向前進的,不會走回頭路。ast

  • 其次要知道滑動窗口其實就是一個隊列,右邊界移動就是有新元素入隊了,左邊界移動就是有元素出隊了,因此在作題的時候能夠想象成一個隊列在進行處理,可能會想的更清楚;

參考連接

滑動窗口的最大值

相關文章
相關標籤/搜索