分享一下刷题记录
滑动窗口的最大值
1.题目链接

2. 题目分析
题目不难理解 ,让一个固定长度为k的窗口在数组中从左往右每次滑动一位, 返回每次窗口中元素的最大值
使用滑动窗口的思路来解决的话, 我们只需要维护一个最大值Max即可, 每次进窗口时更新把新加入的元素和Max进行比较,但是这道题之所以成为困难题就是出现在出窗口这个逻辑上
出窗口时, 窗口内元素一进一出, 这时候如何维护Max的值又成了一个问题,
- 如果窗口left指针出窗口的元素不是最大值的话还好, 因为此时最大值还在窗口中,还处于有效的状态, 新入窗口的元素仍旧可以和它进行比较
- 如果恰好出窗口的元素刚好就是原来维护的Max时, Max就变为无效元素了, 我们就需要找出在
移动之后的窗口内的最大元素, 把它的值赋于Max, 如果使用暴力的解法的话, 时间复杂度则为O(k* n),在窗口长度过大, nums数组元素过多时都会导致超时
下面附上暴力解法:
java
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
List<Integer> list = new ArrayList<>();
int n = nums.length, l = 0, r = 0;
if (n == 1) return new int[]{nums[0]};
int sum = Integer.MIN_VALUE;
for (; r < n; r++) {
sum = Math.max(sum, nums[r]);
// 窗口满
if (r - l + 1 == k) {
list.add(sum); // 1. 先把当前窗口的最大值存起来
// 2. 准备让 l 右移。在右移前,检查即将出窗口的 nums[l] 是否是最大值
if (nums[l] == sum) {
// 如果是,我们需要在 [l+1, r] 这个范围内重新找一个最大值
sum = Integer.MIN_VALUE;
for (int i = l + 1; i <= r; i++) {
sum = Math.max(sum, nums[i]);
}
}
l++; // 3. 左边界正式右移
}
}
return list.stream().mapToInt(Integer::intValue).toArray();
}
}
最终也是不出所料的超出时间限制
使用单调队列就可以很好的解决这个问题,什么是单调队列呢
3. 单调队列
1. 核心特性
单调队列不仅仅是普通队列(FIFO),它允许从队尾进行元素的删除操作。其核心逻辑如下:
-
队列性质 :队列中的元素值必须满足单调性(如:若要求最大值,则队列内元素从队头到队尾必须是严格单调递减的)。
-
元素入队(维持单调性) :当一个新的元素 xxx 进入窗口时:
-
如果 xxx 大于队尾元素,那么队尾元素永远不可能成为当前或后续窗口的最大值,直接将其弹出。
-
重复上述操作,直到队尾元素大于 xxx 或队列为空,然后将 xxx 入队。
-
-
元素出队(过期处理) :由于窗口大小限制为 KKK,如果队头元素已经滑出当前窗口范围(索引小于
当前索引 - K + 1),则将其弹出。
2. 为什么使用它?
-
时间复杂度 :每个元素最多进队一次、出队一次,因此总时间复杂度为 O(N)O(N)O(N) 。相比于暴力求解的 O(N×K)O(N \times K)O(N×K),在大数据量下效率极高。
-
空间复杂度 :O(K)O(K)O(K),最多存储 KKK 个元素。
3.为什么不使用大根堆?
本题的难点在于如何找出移动之后窗口内的最大值,最开始我试图使用大根堆来处理? 来维护一个元素个数为k的大根堆, 但是很快就发现是行不通的,虽然大根堆可以很方便的获取到窗口内的最大值, 但是堆内部的元素顺序和窗口内的元素顺序是不一致的, 这就导致了我们虽然在进窗口时会堆化把最大值置于堆顶,但是出窗口时却需要遍历整个堆来找到要出窗口的那个元素, 所以使用大根堆是行不通的
但是单调队列内部是排资论辈的, 新来的如果比队尾小, 就不会加入队列中, 如果比队尾大则弹出队尾元素取而代之, 这就保证了队尾元素一定是最新加入的元素, 而队首元素一定是最大,资历最老的元素,可以有效的维护窗口在滑动过程中的Max值
4.代码参考(单调队列)
java
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
if(n == 1) return new int []{nums[0]};
int []ret = new int [n - k + 1];
int retIndex = 0;
//使用单调队列
Deque<Integer> deque = new ArrayDeque<>();
for(int r = 0;r < n;r++){
//检查队头元素,排除最大值是否过期
if(!deque.isEmpty() && deque.peekFirst() < r - k + 1){//队首最大值该出窗口了
deque.pollFirst();
}
//新加入窗口的比队尾元素大
while(!deque.isEmpty() && nums[deque.peekLast()] < nums[r]){
deque.pollLast();//队尾弹出
}
//较大值的新元素加入队尾
deque.offerLast(r);
//窗口大小达到k时记录结果
if(r >= k - 1){
ret[retIndex++] = nums[deque.peekFirst()];
}
}
return ret;
}
}