一、题目
给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
进阶:
你能在线性时间复杂度内解决此题吗?
示例:
输入: 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 <= nums.length <= 10^5
- -10^4 <= nums[i] <= 10^4
- 1 <= k <= nums.length
二、解决
1、暴力破解(超时)
思路:
S1、从
i
=
0
i=0
i=0 开始,遍历给定的数组;
S2、对于
i
i
i 开始及之后
k
−
1
k-1
k−1 个位置,形成一个滑动窗口;
S3、在这
k
k
k 个滑动窗口内,取最大值记录下来,放到返回数组内。
代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums.length==0 || k<=0) return new int[0];
int[] res = new int[nums.length-k+1];
for (int i = 0; i < nums.length+1-k; i++) {
int max = Integer.MIN_VALUE;
for (int j = i; j < i+k; j++) {
max = Math.max(max, nums[j]);
}
res[i] = max;
}
return res;
}
}
时间复杂度:
O
(
n
k
)
O(nk)
O(nk)
空间复杂度:
O
(
n
)
O(n)
O(n),ans需要
n
−
k
+
1
n-k+1
n−k+1个空间。
2、最大堆法
思路:
S1:从 i=0 开始遍历数组,并将元素放入最大堆 maxHeap 中;
S2:当 i>=k-1 时,取出首元素放入返回数组中;
S3:当 i>=k 时,一边从 maxHeap 删除超过 k 个的元素,一边添加新元素,直至遍历完。
代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
if (n ==0 || k<=0) return new int[0];
int[] res = new int[n-k+1];
PriorityQueue<int[]> maxHeap = new PriorityQueue<>((a, b)->(b[0]-a[0]));
for (int i = 0; i < n; i++) {
maxHeap.add(new int[]{nums[i], i});
while (i - maxHeap.peek()[1] >= k) {
maxHeap.poll();
}
if (i >= k-1) {
res[i-k+1] = maxHeap.peek()[0];
}
}
return res;
}
}
时间复杂度:
O
(
n
l
o
g
k
)
O(nlogk)
O(nlogk)
空间复杂度:
O
(
n
)
O(n)
O(n)
3、二叉搜索树
思路:
这里主要应用近似平衡二叉树–红黑树的数据结构,理解不是很难,重要的还是方法的了解,过程类似 2-最大堆。
代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums==null || nums.length==0 || k<=0) return new int[0];
int n = nums.length, j = 0;
int[] res = new int[n+1-k];
TreeMap<Integer, Integer> redBlack = new TreeMap<>();
for (int i = 0; i < n; i++) {
redBlack.put(nums[i], redBlack.getOrDefault(nums[i],0)+1); // 基于红黑树--近似平衡,<2倍高度差
if (i+1 >= k) {
res[j++] = redBlack.lastKey();
removeElement(redBlack, nums[i+1-k]);
}
}
return res;
}
private void removeElement(TreeMap<Integer, Integer> redBlack, int x) {
redBlack.put(x, redBlack.getOrDefault(x, 0)-1);
if (redBlack.get(x) == 0) redBlack.remove(x); // 删除key及其映射
}
}
时间复杂度:
O
(
n
l
o
g
k
)
O(nlogk)
O(nlogk)
空间复杂度:
O
(
n
)
O(n)
O(n)
4、双边队列
思路:
过程可以参考6,这里不再赘述。
代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
// assume nums is not null
int n = nums.length;
if (n == 0 || k == 0) {
return new int[0];
}
int[] res = new int[n - k + 1]; // number of windows
Deque<Integer> win = new ArrayDeque<>(); // stores indices
for (int i = 0; i < n; ++i) {
// remove indices that are out of bound
while (win.size() > 0 && i - win.peekFirst() >= k) {
win.pollFirst();
}
// remove indices whose corresponding values are less than nums[i]
while (win.size() > 0 && nums[win.peekLast()] < nums[i]) {
win.pollLast();
}
// add nums[i]
win.offerLast(i);
// add to res
if (i >= k - 1) {
res[i - k + 1] = nums[win.peekFirst()];
}
}
return res;
}
}
附:Deque()方法含义:
offerLast() - 将元素插入双端队列的尾部。
peekFirst() - 从双端队列的头部检索元素。
peekLast() - 从双端队列的尾部检索元素。
pollFirst() - 从双端队列中删除元素。
pollLast() - 从双端队列的尾部删除元素。
时间复杂度: O(n)
空间复杂度: O(k)
5、动态规划
思路:
S1:声明left[n]、right[n]数组,分别记录从左->右 && 右->左 每
k
k
k 个元素的最大值;
S2:遍历数组,然后比较获得left、right数组中每个元素的值,然后将结果存在left、right数组中;
S3:再从头(i=0)开始进行遍历,在
k
k
k 个元素滑动窗口中,将
m
a
x
{
l
e
f
t
[
i
+
k
−
1
]
、
r
i
g
h
t
[
i
]
}
max\{left[i+k-1]、right[i]\}
max{left[i+k−1]、right[i]} 结果记录进output;
- 稍微解释下为什么是 m a x { l e f t [ i + k − 1 ] 、 r i g h t [ i ] } max\{left[i+k-1]、right[i]\} max{left[i+k−1]、right[i]} ?
1)对于任意区间
[
i
,
i
+
k
−
1
]
[i,i+k-1]
[i,i+k−1],可能是1个块,很容易理解最大值就在边界;
2)也可能跨边界,是2个块,这里区间最大值可能分布偏左边,也可能分布在偏右边,这两种情况下,最大值都会影响到最左边或最右边的最大值,可以想象是每K个元素单向传染,结果还是在边界。
S4:遍历完毕后,返回数组output。
更具体可以看参考4中的方法三。
代码:
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
if (n * k == 0) return new int[0];
if (k == 1) return nums;
int[] left = new int[n]; left[0] = nums[0];
int[] right = new int[n]; right[n-1] = nums[n-1];
for (int i = 1; i < n; i++) {
// from left to right
if (i % k == 0) left[i] = nums[i]; // block_start
else left[i] = Math.max(left[i - 1], nums[i]);
// from right to left
int j = n - i - 1;
if ((j + 1) % k == 0) right[j] = nums[j]; // block_end
else right[j] = Math.max(right[j + 1], nums[j]);
}
int[] res = new int[n - k + 1];
for (int i = 0; i < n - k + 1; i++)
res[i] = Math.max(left[i + k - 1], right[i]);
return res;
}
}
时间复杂度:
O
(
n
)
O(n)
O(n)
空间复杂度:
O
(
n
)
O(n)
O(n)
三、参考
1、[Java] All Solutions (B-F, PQ, Deque, DP) with Explanation and Complexity Analysis
2、Important to talk about the solution (Brute Force vs Deque Method) in Java
3、[Java] MaxHeap & BST & Decreasing Monotonic Queue Solutions - Clean code
4、滑动窗口最大值
5、Deque示例
6、🎦【视频解析】 双端队列滑动窗口最大值
7、java PriorityQueue 最小、最大堆(正确版本)
8、Java 8 Lambda 表达式
9、java.util.TreeMap.lastKey()方法实例
10、java.util.TreeMap.remove()方法實例