题目描述
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例 1:
输入: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
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
-
1 <= nums.length <= 10^5 -
-10^4 <= nums[i] <= 10^4 -
1 <= k <= nums.length
解题思路
这道题是滑动窗口类问题的经典题目,需要高效地找出每个窗口的最大值。下面介绍三种解法,从暴力到最优。
解法一:暴力法(不推荐)
最直观的方法是遍历每个窗口,在每个窗口内找出最大值。
算法步骤:
-
窗口从0开始滑动到
n-k -
对每个窗口,遍历窗口内所有元素找最大值
-
将最大值存入结果数组
java
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] result = new int[n - k + 1];
for (int i = 0; i <= n - k; i++) {
int max = Integer.MIN_VALUE;
for (int j = i; j < i + k; j++) {
max = Math.max(max, nums[j]);
}
result[i] = max;
}
return result;
}
复杂度分析:
-
时间复杂度:O(n·k),每个窗口需要O(k)时间找最大值,n个窗口
-
空间复杂度:O(1),不考虑结果数组
解法二:优先队列(最大堆)
利用优先队列(最大堆)来维护窗口内的元素,堆顶就是最大值。
算法步骤:
-
创建一个最大堆,存储
[元素值, 下标]的数组 -
先将前k个元素入堆
-
记录堆顶元素作为第一个窗口的最大值
-
遍历剩余元素:
-
将新元素入堆
-
检查堆顶元素的下标是否在当前窗口内,如果不在则弹出
-
记录当前堆顶元素作为当前窗口的最大值
-
java
import java.util.PriorityQueue;
import java.util.Comparator;
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
// 最大堆,存储 [值, 下标],按值降序,值相同按下标降序
PriorityQueue<int[]> pq = new PriorityQueue<>(new Comparator<int[]>() {
public int compare(int[] a, int[] b) {
return a[0] != b[0] ? b[0] - a[0] : b[1] - a[1];
}
});
// 前k个元素入堆
for (int i = 0; i < k; i++) {
pq.offer(new int[]{nums[i], i});
}
int[] result = new int[n - k + 1];
result[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();
}
result[i - k + 1] = pq.peek()[0];
}
return result;
}
复杂度分析:
解法三:单调队列(最优解)
使用双端队列(Deque)维护一个单调递减的队列,队列中存储的是元素的下标,且这些下标对应的元素值是从队首到队尾递减的。这样队首始终是当前窗口的最大值-3-6-10。
核心思想:
-
如果新元素比队列尾部元素大,说明尾部元素永远不可能成为最大值(因为新元素更大且更靠后),直接弹出尾部元素
-
如果队首元素的下标已经滑出窗口,则弹出队首元素
-
这样队首元素就是当前窗口的最大值
算法步骤:
-
创建双端队列
deque,用于存储下标 -
遍历数组每个元素
nums[i]:-
维护单调性 :当队列不为空且
nums[队尾] < nums[i]时,弹出队尾元素(因为当前元素更大,之前的较小元素不再可能成为最大值) -
加入当前元素 :将当前下标
i加入队尾 -
移除过期元素 :如果队首下标
<= i - k,说明队首元素已经滑出窗口,弹出队首 -
记录结果 :当
i >= k-1时,窗口已形成,队首元素就是当前窗口的最大值
-
java
import java.util.Deque;
import java.util.ArrayDeque;
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0 || k <= 0) {
return new int[0];
}
int n = nums.length;
int[] result = new int[n - k + 1];
// 双端队列,存储下标
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 1. 维护单调递减:移除所有比当前元素小的队尾元素
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 2. 将当前下标加入队尾
deque.offerLast(i);
// 3. 移除已经滑出窗口的队首元素
if (deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 4. 当窗口形成时(i >= k-1),记录最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
举例说明:
以nums = [1,3,-1,-3,5,3,6,7], k = 3为例:
| i | nums[i] | 队列操作 | 队列状态(存储下标) | 对应元素值 | 是否记录结果 |
|---|---|---|---|---|---|
| 0 | 1 | 加入0 | [0] | [1] | 否 |
| 1 | 3 | 3>1,弹出0,加入1 | [1] | [3] | 否 |
| 2 | -1 | -1<3,加入2 | [1,2] | [3,-1] | 是 → 3 |
| 3 | -3 | -3<-1,加入3 | [1,2,3] | [3,-1,-3] | 是 → 3 |
| 4 | 5 | 5> -3, -1, 3,依次弹出3,2,1,加入4 | [4] | [5] | 是 → 5 |
| 5 | 3 | 3<5,加入5 | [4,5] | [5,3] | 是 → 5 |
| 6 | 6 | 6>3,5,弹出5,4,加入6 | [6] | [6] | 是 → 6 |
| 7 | 7 | 7>6,弹出6,加入7 | [7] | [7] | 是 → 7 |
复杂度分析:
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 暴力法 | O(n·k) | O(1) | 思路简单 | 大数据量超时 |
| 优先队列 | O(n log n) | O(n) | 利用堆特性 | 不是最优 |
| 单调队列 | O(n) | O(k) | 线性时间,最优解 | 需要理解单调队列思想 |
单调队列的深入理解
什么是单调队列?
单调队列是一种特殊的队列,队列中的元素保持单调递增或单调递减。本题中我们使用的是单调递减队列 ,即队首到队尾的元素值是递减的-3-6。
为什么使用双端队列?
因为我们需要:
-
从队尾弹出较小的元素(维护单调性)
-
从队首弹出过期的元素(窗口滑动)
-
从队尾加入新元素
核心操作详解
java
// 1. 维护单调性:新元素比队尾元素大,队尾元素永远不可能成为最大值,弹出
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 2. 加入新元素
deque.offerLast(i);
// 3. 移除过期元素:队首元素下标已经滑出窗口
if (deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 4. 获取当前窗口最大值
int max = nums[deque.peekFirst()];
边界条件和注意事项
-
空数组或k=0 :直接返回空数组-10
-
k=1:每个窗口只有一个元素,结果就是原数组
-
k >= 数组长度:整个数组就是一个窗口,返回全局最大值
-
队列存储的是下标而非值:这样可以方便判断元素是否还在窗口内
相关题目推荐
-
力扣59. 滑动窗口的最大值(剑指Offer)
-
力扣155. 最小栈(单调栈思想)
-
力扣739. 每日温度(单调栈)
-
力扣496. 下一个更大元素 I
以上就是力扣239题"滑动窗口最大值"的Java解法详细解析,重点掌握单调队列这一最优解法,它是解决滑动窗口最值问题的核心技巧。如果觉得文章不错,欢迎点赞、收藏、关注三连支持!