1. 程式人生 > 其它 >【劍指offer】59 - I. 滑動視窗的最大值

【劍指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;
    }
}

體會

  • 滑動視窗一共有兩種型別:

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

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

參考連結

滑動視窗的最大值