【LeetCode】239. 滑动窗口的最大值(剑指offer 59-1)
239. 滑动窗口的最大值
知识点:队列;滑动窗口;单调
题目描述
给定一个数组 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;
}
}
- python
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if not nums or not k:
return []
queue = deque()
res = []
for i in range(k):
while len(queue) != 0 and nums[i] > queue[-1]:
queue.pop()
queue.append(nums[i])
res.append(queue[0])
for i in range(k,len(nums)):
if queue[0] == nums[i-k]:
queue.popleft()
while len(queue) != 0 and nums[i] > queue[-1]:
queue.pop()
queue.append(nums[i])
res.append(queue[0])
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;
}
}
体会
-
滑动窗口一共有两种类型:
- 窗口长度可变:这种类型中长度是可以变化的,一个基本的流程就是,右边界长,然后到达某一个条件(比如窗口内的和达到某个值,窗口中出现了重复的元素),这时候右边界停下来,左边界长,然后跳出这个条件(比如窗口内的和又小于目标值了,比如窗口中又没有重复元素了),这时候右边界再去移动;(我们要处理的始终保证窗口内满足某个条件,例如窗口内的值小于某值,窗口内没有重复的,只要不满足了就去移左边界);
- 窗口长度固定,比如说固定一个长度的窗口的时候,那右边界长的时候,左边界也得跟着长,维持一个窗口的恒定值;
-
要始终明白滑动窗口的左右边界是不会出现回退的,两个边界肯定都是朝着一个方向前进的,不会走回头路。
-
其次要知道滑动窗口其实就是一个队列,右边界移动就是有新元素入队了,左边界移动就是有元素出队了,所以在做题的时候可以想象成一个队列在进行处理,可能会想的更清楚;