知识点:队列;滑动窗口;单调数组
给定一个数组 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
其次要知道滑动窗口其实就是一个队列,右边界移动就是有新元素入队了,左边界移动就是有元素出队了,因此在作题的时候能够想象成一个队列在进行处理,可能会想的更清楚;