LC-239.滑动窗口最大值

题目描述

leetcode 困难题

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值 。

示例1:

1
2
3
4
5
6
7
8
9
10
11
输入: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

提示1:

1
2
3
1 <= nums.length <= 10^5
-104 <= nums[i] <= 10^4
1 <= k <= nums.length

单调队列

我们可以用单调队列存储窗口内的元素及其下标,从队首到队尾元素单调递减。那么对于每个窗口的最大值,取队首元素即可。

具体来说,我们每次将右指针后移一位时,将右指针新指向的元素尝试插入队尾,但为了保证队列的单调性,需要不断从队尾弹出元素直到队尾元素大于当前右指针元素,容易发现已经弹出的元素无需重新入队,这是因为这些元素均小于当前右指针元素且都是在其左侧的,也就是说这些元素会更早的离开窗口且更小。而对于移除左指针前一个指向的元素,我们把该操作延后处理,只需要在每次弹出队首元素时,校验其下标合法性即可,如果已经离开窗口则继续弹出队首元素直到遇到一个窗口内的下标。

每个元素最多只会进队一次出队一次,所以时间复杂度为 $O(N)$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<int[]> mono = new ArrayDeque<>();
for(int i = 0; i < k - 1; i++){
while(!mono.isEmpty() && mono.peekLast()[0] <= nums[i]){
mono.pollLast();
}
mono.offerLast(new int[]{nums[i], i});
}
int n = nums.length;
int[] ans = new int[n - k + 1];
for(int left = 0, right = k - 1; right < n; left++, right++){
while(!mono.isEmpty() && mono.peekLast()[0] <= nums[right]){
mono.pollLast();
}
mono.offerLast(new int[]{nums[right], right});
while(mono.peekFirst()[1] < left){
mono.pollFirst();
assert !mono.isEmpty();
}
ans[left] = mono.peekFirst()[0];
}
return ans;
}
}

优先队列/TreeMap

类似的做法还有优先队列和 $TreeMap$ 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
PriorityQueue<Integer> max = new PriorityQueue<>((a, b) -> nums[b] - nums[a]);
for(int i = 0; i < k - 1; i++){
max.offer(i);
}
int n = nums.length;
int[] ans = new int[n - k + 1];
for(int left = 0, right = k - 1; right < n; left++, right++){
max.offer(right);
while(max.peek() < left){
max.poll();
}
ans[left - 0] = nums[max.peek()];
}
return ans;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// TreeMap version
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
TreeMap<Integer, Integer> max = new TreeMap<>((a, b) -> b - a);
for(int i = 0; i < k - 1; i++){
max.merge(nums[i], 1, (old, defaultV) -> ++old);
}
int n = nums.length;
int[] ans = new int[n - k + 1];
for(int left = 0, right = k - 1; right < n; left++, right++){
max.merge(nums[right], 1, (old, defaultV) -> ++old);
ans[left - 0] = max.firstKey();
max.compute(nums[left], (key, value) -> --value == 0 ? null : value);
}
return ans;
}
}

线段树

对于区间查询,也可以使用线段树实现,只需要使用数组元素构建线段树后再查询每个窗口的最大值即可。时间复杂度为 $O(NLogN)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
SegTree segTree = new SegTree(nums);
int n = nums.length;
int[] ans = new int[n - k + 1];
for(int i = 0; i <= n - k; i++){
ans[i] = segTree.query(i, i + k - 1, 0, n - 1, 1);
}
return ans;
}
}

class SegTree {
int[] arr;
int[] org;

public SegTree(int[] org) {
this.org = org;
this.arr = new int[org.length * 4];
build(0, org.length - 1, 1);
}

private void build(int s, int t, int p) {
if (s == t) {
arr[p] = org[s];
return;
}
int mid = s + (t - s >> 1);
build(s, mid, p * 2);
build(mid + 1, t, p * 2 + 1);
arr[p] = Math.max(arr[p * 2], arr[p * 2 + 1]);
}

public int query(int l, int r, int s, int t, int p){
if (l <= s && r >= t) {
return arr[p];
}
int mid = s + (t - s >> 1);
int max = Integer.MIN_VALUE;
if (l <= mid) {
max = Math.max(max, query(l, r, s, mid, p * 2));
}
if (r >= mid + 1) {
max = Math.max(max, query(l, r, mid + 1, t, p * 2 + 1));
}
return max;
}

}

LC-239.滑动窗口最大值
https://wecgwm.github.io/2023/02/18/LC-239-滑动窗口最大值/
作者
yichen
发布于
2023年2月18日
许可协议