leetcode hot 100
滑动窗口最大值
描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
解题思路
1、暴力求解,双层遍历;优化:如果新进入窗口的数大于上一个窗口的最大值,则本窗口最大值为新进入的数。否则,如果从滑动窗口出去的那个数不是上一个窗口的最大值,则此窗口最大值等于上一个窗口最大值,否则本窗口需要一次循环来计算最大值。优化后还是超时。。。复杂度为O(n^2)。
2、优先队列,尝试写了下还是超时,代码贴了如下,思考了一下应该是remove方法不是O(1)的,需要进一步优化,优化方法就是在队列中添加元素的索引信息,如果堆顶元素的索引超过了滑动窗口的范围,则从堆中弹出,剩余的堆顶元素就是窗口内的最大值,复杂度为O(nlogn),因为入堆操作复杂度为O(logn)。
3、单调队列,类似于双指针的思想,队列中存放的元素是数组的索引,新加入队列的元素可以淘汰对应数组值比他小的所有元素,新加入的队列总是放在队尾;队首元素需要判断是否超出窗口范围。时间复杂度为O(n)。
4、分块+预处理。
我们可以将数组 nums 从左到右按照 kkk 个一组进行分组,最后一组中元素的数量可能会不足 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)的时间得到答案。
思想与稀疏表相似,稀疏表解决的是RMQ区间最值问题,一般采用动态规划的方式建表。
代码段
// Java
// 优先队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
// 维护一个长度为k的队列,从大到小
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(Comparator.reverseOrder());
for (int i = 0; i < k; ++i) {
priorityQueue.offer(nums[i]);
}
res[0] = priorityQueue.peek();
for (int i = 1; i <= nums.length - k; ++i) {
priorityQueue.remove(nums[i-1]);
priorityQueue.offer(nums[i+k-1]);
res[i] = priorityQueue.peek();
}
return res;
}
}
// 优化remove方法
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
// 维护一个长度为k的队列,从大到小
PriorityQueue<int[]> priorityQueue = new PriorityQueue<>(new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o2[0] == o1[0] ? o2[1] - o1[1] : o2[0] - o1[0];
}
});
for (int i = 0; i < k; ++i) {
priorityQueue.offer(new int[]{nums[i], i});
}
res[0] = priorityQueue.peek()[0];
for (int i = 1; i <= nums.length - k; ++i) {
while (!priorityQueue.isEmpty() && priorityQueue.peek()[1] < i) {
priorityQueue.poll();
}
priorityQueue.offer(new int[]{nums[i + k - 1], i + k - 1});
res[i] = priorityQueue.peek()[0];
}
return res;
}
}
//单调队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
Deque<Integer> deque = new LinkedList<Integer>();
for (int i = 0; i < k; ++i) {
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast();
}
deque.offerLast(i);
}
int[] ans = new int[n - k + 1];
ans[0] = nums[deque.peekFirst()];
for (int i = k; i < n; ++i) {
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast();
}
deque.offerLast(i);
while (deque.peekFirst() <= i - k) {
deque.pollFirst();
}
ans[i - k + 1] = nums[deque.peekFirst()];
}
return ans;
}
}
// 分块+预处理(稀疏表法)
class Solution {
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;
}
}
// Kotlin
// Python
// Go
// JavaScript
// C++
// Rust
技巧总结
区间最值问题多想想稀疏表法,复杂度可降低至O(logn)