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


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

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

題目描述

給定一個數組 nums 和滑動窗口的大小 k,請找出所有滑動窗口里的最大值。

示例
輸入: 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


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

滑動窗口總體上分成兩類,一類是可變長度的滑動窗口,一類是固定長度的滑動窗口,這道題目就是固定長度的。在遍歷元素時,為了保持窗口的大小固定,右側元素進入窗口后,左側元素要能夠出去。然后直到遍歷結束。
想一下剛才的過程,右側元素進入,左側元素出去,這不就是雙端隊列嗎?所以這道題目可以借助雙端隊列來解;

想一下我們經常會遇到求一個隊列或者一個窗口一個棧內的最大最小值,怎么求呢,最簡單的方法就是遍歷這個窗口這個棧,這樣時間復雜度就是O(N),有沒有辦法能在O(1)時間內獲得一個棧或者一個窗口內的最值呢,這其實就是劍指offer30題,比如獲取一個棧內的最小值,我們可以采用一個輔助棧,這個輔助棧有一個最大的特點就是單調的,也就是我們俗稱的單調棧。比如我們維持一個單調遞減棧,如果當前值比棧頂元素大,那就不要了,因為我們最后只獲取最小值,如果比當前棧頂小,那就入棧,也就是更新了最小值;這樣就可以在O(1)的時間內獲得棧內最小值了,因為最小值就是輔助棧的棧頂。

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

比如[5,3,4], 4要入隊的時候發現3比其小,所以3從隊尾出去,4入隊;

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;
    }
}

解法二:滑動窗口+單調

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

流程

  • 遍歷給定數組中的元素,如果隊列不為空且當前考察元素大於等於隊尾元素,則將隊尾元素移除。直到,隊列為空或當前考察元素小於新的隊尾元素;
  • 當隊首元素的下標小於滑動窗口左側邊界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;
    }
}

體會

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

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

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

參考鏈接

滑動窗口的最大值


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM