滑动窗口最大值
题目链接
方法1:暴力求解:
对于每个滑动窗口,我们可以使用 O(k) 的时间遍历其中的每一个元素,找出其中的最大值。
对于长度为 n 的数组 nums ,窗口的数量为 n-k+1
因此该算法的时间复杂度为 O((n-k+1)k)=O(nk),会超出时间限制,需要进行一些优化。
暴力优化:
滑动窗口特点就是每次移动,会出一个元素,进一个元素。
也就是说它们共用着 k-1个元素,而只有 1 个元素是变化的。我们可以根据这个特点进行优化。
首先最容易想到的就是去维护一个最大值,窗口每滑动一次,我去更新这个最大值。显然只维护一个最大值的话,当最大值出去的时候,我需要找到第二大值来更新最大值。
那如果我维护来一个第二大值呢?同样,我还是需要去不断维护这个第二大值,就必然存在当第二大值出去或者成为第一大值的时候,我需要找到下一个第二大值,也就是第三大值,不难推理,有了第三大就还需要第四大…
就维护第一大第二大值为例,做了所有情况的预判,流程图如下:
标红的情况就是需要第三大值的情况。
综上可见:此路不通…
其实我们就是在解决这道题目的一个难点:如何维护一个最大值/如果用最短的时间找到最大值
上面我们企图通过保存特定值,来将找最大值的算法优化成常数时间,经过上面的分析,显然是不行的
而对于找最大值,最暴力的就是每次遍历全部元素需要o(n)的时间复杂度, 其实有点点像排序算法的优化,只不过我们这里只要最大值即可。如果能想到这里,我相信应该就很容易想到用堆这种数据结构,尝试用大顶堆来将这部分算法优化至o(lgn)
方法2:优先队列方法(大顶堆)
什么是优先队列呢?
优先队列就是不再遵循先入先出的原则,而是分为两种情况:
最大优先队列,无论入队顺序,当前最大的元素优先出队。
最小优先队列,无论入队顺序,当前最小的元素优先出队。
其实就是一个大顶堆/小顶堆实现的。显然这种数据结构,完美地帮我们解决了这道题目的一个难点:维护一个最大值
对应思路和算法:
初始时,我们将数组 nums 的前 k个元素放入优先队列(大顶堆)中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。
然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其从优先队列中移除。
我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。
所以我们还需要保存元素的下标,才能让我们去判断它是否在滑动窗口内。
以[ 1,3,-1,-3,5,3,6,7]为例:
时间复杂度:O(nlogn),其中 n是数组nums 的长度。在最坏情况下,数组 nums 中的元素单调递增,那么最终优先队列中包含了所有元素,没有元素被移除。由于将一个元素放入优先队列的时间复杂度为 O(logn),因此总时间复杂度为 O(nlogn)。
空间复杂度:O(n),即为优先队列需要使用的空间。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] pair1, int[] pair2) {
return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
}
});
for (int i = 0; i < k; ++i) {
pq.offer(new int[]{nums[i], i});
}
int[] ans = new int[n - k + 1];
ans[0] = pq.peek()[0];
for (int i = k; i < n; ++i) {
pq.offer(new int[]{nums[i], i});
while (pq.peek()[1] <= i - k) {
pq.poll();
}
ans[i - k + 1] = pq.peek()[0];
}
return ans;
}
}
方法3:单调双端队列
什么是单调双端队列?
就是可以从队头出队,也可以从队尾出队,即不用遵循先进先出的规则,并且队列内的元素是有序的(从小到大/从大到小)
对应思路与算法:
1.首先把我们第一个窗口的所有值依次存入单调双端队列中,单调队列里面的值为单调递减的。
怎么保持队列元素的递减性呢?就是在元素入队的时候,如果发现队尾元素小于要加入的元素,则将队尾元素出队,直到队尾元素大于新元素时,再让新元素入队,目的就是维护一个单调递减的队列。
2.我们将第一个窗口的所有值,按照单调队列的规则入队之后,因为队列为单调递减,所以队头元素必为当前窗口的最大值,则将队头元素添加到结果数组中。
3.移动窗口,判断当前窗口的前一个元素是否和队头元素相等,如果相等则出队。
4.继续然后按照规则进行入队,维护单调递减队列。
5.判断对头元素是否在滑动窗口内,进行正确的出队操作后,将队头元素存到对应的结果数组里。
5.返回结果数组
public int[] maxSlidingWindow(int[] nums, int k) {
int len = nums.length;
if (len == 0) {
return nums;
}
int[] arr = new int[len - k + 1];
int arr_index = 0;
//单调双向队列
Deque<Integer> deque = new LinkedList<>();
//第一个窗口的值按照规则入队
for (int i = 0; i < k; i++) {
while (!deque.isEmpty() && deque.peekLast() < nums[i]) {
deque.removeLast();
}
deque.offerLast(nums[i]);
}
//存到数组里,队头元素
arr[arr_index++] = deque.peekFirst();
//移动窗口
for (int j = k; j < len; j++) {
//对应上面蓝色情况,移出队头元素
if (nums[j - k] == deque.peekFirst()) {
deque.removeFirst();
}
while (!deque.isEmpty() && deque.peekLast() < nums[j]) {
deque.removeLast();
}
deque.offerLast(nums[j]);
arr[arr_index++] = deque.peekFirst();
}
return arr;
}
方法4:分块 + 预处理
思路与算法
首先分组。我们可以将数组 nums 从左到右按照 k 个一组进行分组,最后一组中元素的数量可能会不足 k 个。
如果我们希望求出 nums[i] 到 nums[i+k−1] 的最大值,就会有两种情况:
如果 i 是 k的倍数,那么nums[i] 到nums[i+k−1] 恰好是一个分组。我们只要预处理出每个分组中的最大值,即可得到答案;
如果 i不是 k的倍数,那么nums[i] 到 nums[i+k−1] 会跨越两个分组,
假设 j是 k 的倍数,并且满足 i < j ≤i+k−1,那么 nums[i] 到 nums[j−1] 就是第一个分组的后缀,nums[j] 到 nums[i+k−1] 就是第二个分组的前缀。(看图比较好理解)
如果我们能够预处理出每个分组中的前缀最大值以及后缀最大值,同样可以在 O(1)的时间得到答案。
在预处理完成之后,对于 nums[i] 到nums[i+k−1] 的所有元素,
如果 i不是 k 的倍数,那么窗口中的最大值为 suffixMax[i] 与 prefixMax[i+k−1] 中的较大值;
如果 i 是 k 的倍数,那么此时窗口恰好对应一整个分组,suffixMax[i] 和 prefixMax[i+k−1] 都等于分组中的最大值。
因此无论窗口属于哪一种情况,max{suffixMax[i],prefixMax[i+k−1]}都为滑动窗口最大值。
示例:
1:将8个元素按k分组,分为3组
2:计算出每组的suffixMax,prefixMax
3:开始遍历,无论i是否为k的整数倍,当前滑动窗口的max始终等于max{suffixMax[i],prefixMax[i+k−1]}
时间复杂度:O(n),分别需要 O(n) 的时间预处理出数组 prefixMax,suffixMax 。
空间复杂度:O(n) prefixMax 和 suffixMax 需要的空间
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] prefixMax = new int[n];
int[] suffixMax = new int[n];
for (int i = 0; i < n; ++i) {
if (i % k == 0) {
prefixMax[i] = nums[i];
}
else {
prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);
}
}
for (int i = n - 1; i >= 0; --i) {
if (i == n - 1 || (i + 1) % k == 0) {
suffixMax[i] = nums[i];
} else {
suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);
}
}
int[] ans = new int[n - k + 1];
for (int i = 0; i <= n - k; ++i) {
ans[i] = Math.max(suffixMax[i], prefixMax[i + k - 1]);
}
return ans;
}