知識點:隊列;滑動窗口;單調數組
給定一個數組 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
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
其次要知道滑動窗口其實就是一個隊列,右邊界移動就是有新元素入隊了,左邊界移動就是有元素出隊了,因此在作題的時候能夠想象成一個隊列在進行處理,可能會想的更清楚;