LeetCode 239 滑动窗口最大值
滑动窗口最大值问题详解
问题背景
滑动窗口最大值问题是一个经典的算法问题,通常用于处理数组或列表数据。该问题要求我们找到一个数组中每个长度为 k 的连续子数组(滑动窗口)的最大值,并返回这些最大值组成的数组。这个问题在处理时间序列数据、实时数据流以及图像处理等领域中非常常见。
问题描述
给定一个整数数组 nums
和一个整数 k
,表示滑动窗口的大小。滑动窗口从数组的最左侧开始,每次向右移动一位,直到到达数组的最右侧。对于每个位置的滑动窗口,我们需要找到其中的最大值,并将这些最大值按顺序存储在结果数组中。
暴力解法
最直观的思路是对于数组中的每个位置的窗口,依次遍历其中的元素,找到最大值。这种方法简单直接,但在窗口较大时效率较低。
实现思路
-
遍历数组,对于每个起始位置 i,计算窗口结束位置 i+k−1。
-
在子数组 nums[i...i+k−1] 中找到最大值。
-
将最大值存入结果数组。
实现代码
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;int[] result = new int[n - k + 1];for (int i = 0; i < n - k + 1; i++) {int max = nums[i];for (int j = i; j < i + k; j++) {if (nums[j] > max) {max = nums[j];}}result[i] = max;}return result;}
}
时间复杂度
该方法的时间复杂度为 O(n×k),其中 n 是数组长度。当 k 接近 n 时,时间复杂度接近 O(n2),效率较低。
优化方法:双端队列
为了提高效率,我们可以利用双端队列(Deque)来优化滑动窗口最大值的计算。双端队列可以帮助我们在每个窗口中快速找到最大值,从而将时间复杂度降至 O(n)。
关键思想
-
维护单调递减队列:队列中存储数组元素的索引,并且这些索引对应的值是单调递减的。
-
移除无效元素:当新元素进入窗口时,移除队列中所有比新元素小的元素,因为它们不可能成为后续窗口的最大值。
-
移除超出窗口范围的元素:当窗口滑动时,检查队列头部的元素是否仍在窗口范围内,若不在则移除。
详细步骤
-
初始化一个双端队列,用于存储可能成为最大值的元素的索引。
-
遍历数组中的每个元素:
-
移除队列尾部所有比当前元素小的索引,因为它们不可能成为后续窗口的最大值。
-
将当前元素的索引添加到队列尾部。
-
移除队列头部超出当前窗口范围的索引。
-
当窗口形成(即索引达到 k−1 时),将队列头部的元素值添加到结果数组中。
-
实现代码
import java.util.Deque;
import java.util.LinkedList;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;Deque<Integer> deque = new LinkedList<>();int[] result = new int[n - k + 1];int resultIndex = 0;for (int i = 0; i < n; i++) {// Remove elements smaller than current from the end of dequewhile (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {deque.pollLast();}deque.offerLast(i);// Remove elements out of the window from the front of dequewhile (deque.peekFirst() <= i - k) {deque.pollFirst();}// Start adding to result when the window is formedif (i >= k - 1) {result[resultIndex++] = nums[deque.peekFirst()];}}return result;}
}
算法分析
时间复杂度
每个元素最多被添加和移除一次,因此时间复杂度为 O(n)。
空间复杂度
双端队列存储的元素数量最多为 k,因此空间复杂度为 O(k)。
示例
给定数组 nums = [1, 3, -1, -3, 5, 3, 6, 7]
和 k = 3
,滑动窗口的最大值数组为 [3, 3, 5, 5, 6, 7]
。
详细过程
-
初始状态:
deque
为空,result
为空。 -
遍历数组:
-
i=0:添加索引 0 到
deque
。deque = [0]
。 -
i=1:移除比 3 小的元素(索引 0),添加索引 1 到
deque
。deque = [1]
。 -
i=2:添加索引 2 到
deque
。deque = [1, 2]
。窗口形成,result[0] = nums[1] = 3
。 -
i=3:移除比 -3 小的元素(无),添加索引 3 到
deque
。deque = [1, 2, 3]
。移除超出窗口范围的索引 1。deque = [2, 3]
。result[1] = nums[2] = -1
?不对,需要重新检查。
似乎在解释过程中出现了一个错误,让我重新详细地走一遍这个例子:
-
实际上,在例子 nums = [1, 3, -1, -3, 5, 3, 6, 7]
和 k = 3
中,正确的结果应该是 [3, 3, 5, 5, 6, 7]
。让我们详细模拟每一步:
-
i = 0:
-
nums[i] = 1
-
deque
为空,直接添加索引 0。 -
deque = [0]
-
窗口尚未形成(i < k-1),不添加到结果。
-
-
i = 1:
-
nums[i] = 3
-
移除
deque
尾部比 3 小的索引(0对应的值1 < 3,所以移除0)。 -
添加索引1到
deque
。 -
deque = [1]
-
窗口尚未形成(i < k-1),不添加到结果。
-
-
i = 2:
-
nums[i] = -1
-
deque
尾部是索引1对应的值3,比-1大,所以直接添加索引2。 -
deque = [1, 2]
-
窗口形成(i >= k-1),结果添加
nums[deque.front()] = nums[1] = 3
。 -
result = [3]
-
-
i = 3:
-
nums[i] = -3
-
deque
尾部是索引2对应的值-1,比-3大,所以直接添加索引3。 -
deque = [1, 2, 3]
-
检查窗口范围:
deque.front() = 1
,而 i−k+1=3−3+1=1,所以索引1在窗口范围内。 -
结果添加
nums[1] = 3
。 -
result = [3, 3]
-
-
i = 4:
-
nums[i] = 5
-
移除
deque
尾部比5小的元素:索引3(-3)和索引2(-1)都比5小,所以移除。 -
移除后
deque = [1]
-
添加索引4到
deque
。 -
deque = [1, 4]
-
检查窗口范围:
deque.front() = 1
,而 i−k+1=4−3+1=2,索引1 < 2,所以移除索引1。 -
deque = [4]
-
结果添加
nums[4] = 5
。 -
result = [3, 3, 5]
-
-
i = 5:
-
nums[i] = 3
-
deque
尾部是索引4对应的值5,比3大,所以直接添加索引5。 -
deque = [4, 5]
-
检查窗口范围:i−k+1=5−3+1=3,索引4 >= 3,所以无需移除。
-
结果添加
nums[4] = 5
。 -
result = [3, 3, 5, 5]
-
-
i = 6:
-
nums[i] = 6
-
移除
deque
尾部比6小的元素:索引5(3)和索引4(5)都比6小,所以移除。 -
deque
变为空。 -
添加索引6到
deque
。 -
检查窗口范围:i−k+1=6−3+1=4,索引6 >=4。
-
结果添加
nums[6] = 6
。 -
result = [3, 3, 5, 5, 6]
-
-
i = 7:
-
nums[i] = 7
-
移除
deque
尾部比7小的元素:索引6(6)比7小,移除。 -
deque
变为空。 -
添加索引7到
deque
。 -
检查窗口范围:i−k+1=7−3+1=5,索引7 >=5。
-
结果添加
nums[7] = 7
。 -
result = [3, 3, 5, 5, 6, 7]
-
这样,最终结果为 [3, 3, 5, 5, 6, 7]
。
应用场景
滑动窗口最大值问题在许多实际场景中都有应用,例如:
-
实时数据处理:在实时数据流中,需要快速找到最近一段时间内的最大值。例如,监控系统中每秒检测数据,需要实时显示过去5秒的最大值。
-
图像处理:在图像处理中,滑动窗口技术用于特征提取和滤波。例如,最大值滤波可以用于增强图像中的亮点。
-
时间序列分析:在金融数据、传感器数据等时间序列分析中,滑动窗口最大值可以帮助识别趋势和异常。例如,股票价格分析中,可以使用滑动窗口找到过去7天的最高价。
总结
滑动窗口最大值问题是一个经典的算法问题,通过使用双端队列可以高效地解决。这种方法不仅提高了算法的效率,还展示了数据结构在优化算法中的重要性。希望这篇文章能帮助你更好地理解和应用滑动窗口技术。
在实际应用中,选择合适的算法和数据结构至关重要。对于滑动窗口问题,双端队列提供了一种高效且优雅的解决方案。通过维护一个单调递减的队列,我们可以在 O(n) 的时间复杂度内完成计算,这对于处理大规模数据尤为重要。