题目:
官方链接:
参考答案:
【新手入门】LeetCode 239. 滑动窗口最大值:Java & JavaScript 双解法详解
目录
1. 题目描述
给定一个整数数组 nums
和一个整数 k
,有一个大小为 k
的滑动窗口从数组的最左侧移动到最右侧。每次滑动窗口向右移动一位,返回每个滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
示例 2:
输入:nums = [1], k = 1
输出:[1]
2. 问题分析
- 滑动窗口:窗口每次右移一位,需要高效计算窗口内的最大值。
- 性能要求:
- 暴力法时间复杂度为 O(nk),无法通过大规模数据测试(
n ≤ 10^5
)。 - 需要优化到 O(n) 时间复杂度。
- 暴力法时间复杂度为 O(nk),无法通过大规模数据测试(
- 核心挑战:如何在窗口滑动时高效维护最大值。
3. 解题思路
3.1 暴力法(不推荐)
- 思路:对每个窗口遍历所有元素找最大值。
- 缺点:时间复杂度 O(nk),无法通过测试。
3.2 单调队列法(最优解)
核心思想:
- 使用 双端队列(Deque) 存储当前窗口内可能成为最大值的元素的索引。
- 队列保持单调递减(队首始终是当前窗口的最大值)。
操作流程:
- 初始化队列:处理前
k
个元素,维护单调递减队列。 - 滑动窗口:
- 移除队列中不在窗口范围内的索引(从队首开始检查)。
- 移除队列中比当前元素小的索引(从队尾开始检查)。
- 将当前元素索引加入队尾。
- 记录最大值:每次窗口移动后,队首元素即为当前窗口的最大值。
优势:
- 每个元素最多入队和出队一次,时间复杂度 O(n)。
- 空间复杂度 O(k)(队列最多存储
k
个元素)。
4. Java代码实现
import java.util.*;
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];
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 移除不在窗口范围内的索引
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 移除比当前元素小的索引
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 加入当前索引
deque.offerLast(i);
// 记录窗口最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
}
5. JavaScript代码实现
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
if (!nums.length || k === 0) return [];
const n = nums.length;
const result = [];
const deque = [];
for (let i = 0; i < n; i++) {
// 移除不在窗口范围内的索引
while (deque.length > 0 && deque[0] < i - k + 1) {
deque.shift();
}
// 移除比当前元素小的索引
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
// 加入当前索引
deque.push(i);
// 记录窗口最大值
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
};
6. 复杂度分析
方法 | 时间复杂度 | 空间复杂度 |
---|---|---|
暴力法 | O(nk) | O(1) |
单调队列法 | O(n) | O(k) |
7. 边界条件与注意事项
- 空数组或 k=0:直接返回空数组。
- k=1:每个窗口只有一个元素,直接返回原数组。
- k ≥ nums.length:整个数组就是一个窗口,返回全局最大值。
- 队列操作:
- Java 使用
Deque
接口的实现类ArrayDeque
。 - JavaScript 使用数组模拟双端队列(注意
shift()
和pop()
的性能)。
- Java 使用
8. 总结
- 面试推荐:单调队列法是标准解法,考察对数据结构的灵活运用。
- 核心技巧:
- 使用双端队列维护窗口内的可能最大值。
- 队首始终是当前窗口的最大值。
- 及时移除无效索引(不在窗口内或比当前元素小)。
- 适用场景:需要高效计算滑动窗口最值的场景(如股票分析、实时数据流)。
掌握这道题,你就能高效解决滑动窗口最大值问题! 🚀
博客标题:滑动窗口最大值问题:Java与JavaScript解决方案
目录
1. 引言
滑动窗口最大值问题是算法中的一个常见问题,它要求我们在一个固定大小的窗口中找到最大值。在每次窗口滑动时,我们需要更新最大值。本文将分别使用Java和JavaScript来解答这个问题。
2. 问题分析
给定一个整数数组 nums
和一个整数 k
,表示窗口的大小。我们需要计算每个窗口中的最大值,并返回一个包含所有窗口最大值的数组。
3. Java实现
3.1 算法思路
我们可以使用一个双端队列(Deque)来维护窗口中的最大值。具体步骤如下:
- 初始化一个双端队列
deque
,它将存储窗口中的元素索引。 - 遍历数组
nums
:- 对于每个元素,首先从队列的尾部移除所有小于当前元素的值,因为这些值不可能是后续窗口的最大值。
- 将当前元素的索引添加到队列的尾部。
- 如果队列的头部索引超出了当前窗口的范围,则将其移除。
- 每次迭代时,队列的头部存储的是当前窗口的最大值的索引。
- 返回一个数组,其中包含每个窗口的最大值。
3.2 代码实现
import java.util.ArrayDeque;
import java.util.Deque;
public class MaxSlidingWindow {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0 || k <= 0) {
return new int[0];
}
Deque<Integer> deque = new ArrayDeque<>();
int[] result = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
// 移除不在窗口内的元素
if (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 移除小于当前元素的值
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 添加当前元素的索引
deque.offerLast(i);
// 如果窗口已满,则记录最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
}
4. JavaScript实现
4.1 算法思路
JavaScript的实现与Java类似,只是语法上有所不同。
4.2 代码实现
function maxSlidingWindow(nums, k) {
if (!nums || nums.length === 0 || k <= 0) {
return [];
}
let deque = [];
let result = [];
for (let i = 0; i < nums.length; i++) {
// 移除不在窗口内的元素
if (deque.length && deque[0] < i - k + 1) {
deque.shift();
}
// 移除小于当前元素的值
while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
// 添加当前元素的索引
deque.push(i);
// 如果窗口已满,则记录最大值
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
}
5. 总结
通过本文,我们学习了如何使用Java和JavaScript解决滑动窗口最大值问题。使用双端队列来维护窗口中的最大值是一种有效的方法,它允许我们在O(n)的时间复杂度内完成整个数组的遍历。希望这篇文章能帮助你更好地理解并掌握这一知识点。
好的,我很乐意为你提供详细的解答和博客内容。以下是一篇新手入门博客,包含Java和JavaScript的解答以及相关知识点的详细介绍。
博客标题: 从零开始学习力扣题 239. 滑动窗口最大值
目录:
-
问题描述
-
Java解答
2.1 使用双端队列 -
JavaScript解答
3.1 使用双端队列 -
知识点总结
4.1 滑动窗口
4.2 双端队列
4.3 时间复杂度和空间复杂度 -
问题描述
力扣题 239. 滑动窗口最大值要求实现一个函数maxSlidingWindow(nums, k)
,其中nums
是一个整数数组,k
是滑动窗口的大小。该函数需要返回滑动窗口中的最大值。 -
Java解答
2.1 使用双端队列
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) {
return new int[0];
}
int n = nums.length;
int[] result = new int[n - k + 1];
Deque<Integer> deque = new LinkedList<>();
for (int i = 0; i < n; i++) {
// 移除队列中小于当前元素的值
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 添加当前元素索引
deque.offerLast(i);
// 移除队列中超出滑动窗口的元素
if (deque.peekFirst() == i - k) {
deque.pollFirst();
}
// 当滑动窗口大小达到 k 时,将最大值添加到结果数组
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
}
- JavaScript解答
3.1 使用双端队列
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
if (!nums || nums.length === 0) {
return [];
}
const n = nums.length;
const result = [];
const deque = [];
for (let i = 0; i < n; i++) {
// 移除队列中小于当前元素的值
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
// 添加当前元素索引
deque.push(i);
// 移除队列中超出滑动窗口的元素
if (deque[0] === i - k) {
deque.shift();
}
// 当滑动窗口大小达到 k 时,将最大值添加到结果数组
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
};
- 知识点总结
4.1 滑动窗口
滑动窗口是一种常见的数据结构,它可以用来解决一些需要维护一个固定大小窗口的问题,比如求最大值、最小值等。
4.2 双端队列
双端队列是一种特殊的队列,它允许在队列的两端进行插入和删除操作。在本题中,我们使用双端队列来维护滑动窗口中的最大值。
4.3 时间复杂度和空间复杂度
maxSlidingWindow()
方法的时间复杂度为 O(n),空间复杂度为 O(k),其中 n 是数组的长度,k 是滑动窗口的大小。
滑动窗口最大值:Java与JavaScript详解(LeetCode 239)
目录
- 引言
- 题目简介
- 解题思路
- Java实现详解
- JavaScript实现详解
- 其他优化技巧
- 结语
1. 引言
在处理连续区间最大值的问题时,“滑动窗口最大值”是经典也是非常实用的一题。它不仅考察你的数据结构基础,还锻炼你的算法优化能力。本文将逐步讲解此题的思路,并用Java和JavaScript两种语言完整实现,帮助你理解和掌握解决方案。
2. 题目简介
题目描述:
给定一个数组nums
和一个滑动窗口大小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
3. 解题思路
方法一:使用双端队列(核心方案)
利用一个双端队列(Deque)存储当前窗口可能的最大值的索引:
- 保持队列索引对应的值按降序排列,队首为最大值。
- 当滑动窗口向右移动:
- 弹出队尾中所有比新元素小的索引(因为它们不可能成为最大值)
- 将新元素索引加入队尾
- 如果队首元素已经超出窗口范围(索引小于
i - k + 1
),就弹出队首 - 当前窗口最大值即队首元素对应的值
优点:
- 时间复杂度:O(n),每个元素最多入队出队一次。
4. Java实现详解
import java.util.Deque;
import java.util.LinkedList;
public class SlidingWindowMaximum {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || k == 0) return new int[0];
int n = nums.length;
int[] result = new int[n - k + 1];
Deque<Integer> deque = new LinkedList<>();
for (int i = 0; i < n; i++) {
// 移除队尾所有比当前元素小的索引
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
// 移除超出窗口范围的索引
if (deque.peekFirst() == i - k) {
deque.pollFirst();
}
// 收集结果
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
public static void main(String[] args) {
SlidingWindowMaximum solution = new SlidingWindowMaximum();
int[] nums = {1,3,-1,-3,5,3,6,7};
int[] res = solution.maxSlidingWindow(nums, 3);
for (int v : res) {
System.out.print(v + " ");
}
// 输出:3 3 5 5 6 7
}
}
5. JavaScript实现详解
var maxSlidingWindow = function(nums, k) {
const result = [];
const deque = []; // 存索引
for (let i = 0; i < nums.length; i++) {
// 移除队尾所有比当前元素小的索引
while (deque.length && nums[deque[deque.length -1]] < nums[i]) {
deque.pop();
}
deque.push(i);
// 移除超出窗口范围的索引
if (deque[0] === i - k) {
deque.shift();
}
// 收集结果
if (i >= k -1) {
result.push(nums[deque[0]]);
}
}
return result;
};
// 示例测试
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3)); // [3,3,5,5,6,7]
6. 其他优化技巧
- 用单调队列:保证队列中的元素单调递减,提高效率。
- 不同数据结构选择:优先队列、平衡树等也可以实现,但双端队列最简洁高效。
7. 结语
利用双端队列(队列的单调性特点)是解决“滑动窗口最大值”问题的标准方案。这种技巧不仅在此题中有用,还可以应用到诸如滑动窗口平均值、区间判定等问题中。理解和掌握这种数据结构的用法,将极大增强你的算法能力。
祝你练习顺利,算法之路越走越宽!
力扣题解:239. 滑动窗口最大值
目录
- 题目描述
- 解题思路
- 2.1 暴力解法
- 2.2 双端队列解法
- Java 实现
- JavaScript 实现
- 总结
1. 题目描述
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回滑动窗口中的最大值。
示例
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
提示
- (1 \leq \text{nums.length} \leq 10^5)
- (-10^4 \leq \text{nums[i]} \leq 10^4)
- (1 \leq k \leq \text{nums.length})
2. 解题思路
2.1 暴力解法
暴力解法的思路是,对于每一个滑动窗口,遍历窗口内的元素,找到最大值。这个方法的时间复杂度为 (O(n \cdot k)),在最坏情况下可能会超时。
2.2 双端队列解法
为了提高效率,我们可以使用双端队列(Deque)来维护当前滑动窗口的最大值。具体步骤如下:
- 维护一个双端队列:队列中的元素是数组的索引,且队列中的元素是按值从大到小排列的。
- 滑动窗口的移动:
- 在每次移动窗口时,首先移除队列中不在当前窗口的元素。
- 然后,移除队列中所有小于当前元素的索引(因为它们不可能是最大值)。
- 将当前元素的索引添加到队列中。
- 当窗口的大小达到
k
时,队列的头部就是当前窗口的最大值。
这种方法的时间复杂度为 (O(n)),因为每个元素最多被添加和删除一次。
3. Java 实现
以下是 Java 的实现代码:
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 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++) {
// Remove elements not in the sliding window
if (!deque.isEmpty() && deque.peek() < i - k + 1) {
deque.poll();
}
// Remove elements smaller than the current element
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// Add current element's index
deque.offer(i);
// Add the maximum for the current window
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peek()];
}
}
return result;
}
}
4. JavaScript 实现
以下是 JavaScript 的实现代码:
var maxSlidingWindow = function(nums, k) {
if (nums.length === 0) return [];
const result = [];
const deque = []; // 存储索引
for (let i = 0; i < nums.length; i++) {
// 移除不在滑动窗口的元素
if (deque.length && deque[0] < i - k + 1) {
deque.shift();
}
// 移除所有小于当前元素的索引
while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
// 添加当前元素的索引
deque.push(i);
// 添加当前窗口的最大值
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
};
5. 总结
在这篇博客中,我们讨论了力扣题目“239. 滑动窗口最大值”的解法。我们首先介绍了题目的基本要求,然后分析了使用双端队列的方法来高效地找到滑动窗口中的最大值。最后,我们提供了 Java 和 JavaScript 的实现代码。
通过这种方法,我们能够在 (O(n)) 的时间复杂度内处理数据流,适合处理较大的输入数据。希望这篇博客能帮助你更好地理解这个问题及其解法!如果你有任何问题或想法,欢迎在评论区留言讨论。
好的,为你准备一篇关于 LeetCode 239. 滑动窗口最大值的详细入门博客,包含 Java 和 JavaScript 的解法,并深入讲解算法思路和复杂度分析。
博客标题: LeetCode 239. 滑动窗口最大值:新手入门指南 (Java & JavaScript)
目录:
- 引言:滑动窗口中的最大值
- 题目描述
- 解题思路:单调队列
- 3.1 为什么选择单调队列?
- 3.2 算法步骤
- Java 代码实现
- 4.1 代码
- 4.2 代码解释
- JavaScript 代码实现
- 5.1 代码
- 5.2 代码解释
- 复杂度分析
- 6.1 时间复杂度
- 6.2 空间复杂度
- 总结:掌握单调队列,高效解决滑动窗口问题!
博客正文:
1. 引言:滑动窗口中的最大值
“滑动窗口最大值”是 LeetCode 上一道经典的困难难度题目。它考察了你对队列这种数据结构的理解和应用,以及你解决问题的能力。掌握这道题的解法,对于你提升算法思维非常有帮助。
2. 题目描述
给你一个整数数组 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
输入:nums = [1], k = 1
输出:[1]
3. 解题思路:单调队列
-
3.1 为什么选择单调队列?
- 高效维护最大值: 单调队列可以高效地维护滑动窗口中的最大值。
- 动态调整: 单调队列可以在 O(1) 的时间复杂度内插入和删除元素,这使得它非常适合处理滑动窗口问题。
- 避免重复比较: 单调队列可以避免重复比较,从而提高效率。
-
3.2 算法步骤
- 创建一个双端队列(
deque
),用于存储数组的索引。 - 遍历数组
nums
。 - 对于每个元素
nums[i]
:- 将队列中所有小于
nums[i]
的元素都移除。 这是为了保证队列是单调递减的。 - 将
i
添加到队列的末尾。 - 如果队列的头部元素已经不在滑动窗口中(即
deque.peekFirst() < i - k + 1
),将队列的头部元素移除。 - 如果
i >= k - 1
,将队列的头部元素(即当前滑动窗口的最大值的索引)对应的nums[deque.peekFirst()]
添加到结果数组中。
- 将队列中所有小于
- 返回结果数组。
- 创建一个双端队列(
4. Java 代码实现
-
4.1 代码
import java.util.Deque; import java.util.LinkedList; 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; Deque<Integer> deque = new LinkedList<>(); int[] result = new int[n - k + 1]; for (int i = 0; i < n; i++) { // 移除队列中小于当前元素的元素 while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) { deque.removeLast(); } // 添加当前元素的索引到队列 deque.addLast(i); // 移除队列中不在滑动窗口中的元素 if (deque.peekFirst() < i - k + 1) { deque.removeFirst(); } // 如果滑动窗口已经形成,将最大值添加到结果数组 if (i >= k - 1) { result[i - k + 1] = nums[deque.peekFirst()]; } } return result; } }
-
4.2 代码解释
Deque<Integer> deque = new LinkedList<>();
: 创建一个双端队列。while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) { deque.removeLast(); }
: 移除队列中小于当前元素的元素。deque.addLast(i);
: 添加当前元素的索引到队列。if (deque.peekFirst() < i - k + 1) { deque.removeFirst(); }
: 移除队列中不在滑动窗口中的元素。if (i >= k - 1) { result[i - k + 1] = nums[deque.peekFirst()]; }
: 如果滑动窗口已经形成,将最大值添加到结果数组。
5. JavaScript 代码实现
-
5.1 代码
function maxSlidingWindow(nums, k) { const n = nums.length; if (n * k === 0) return []; if (k === 1) return nums; const deque = []; const result = new Array(n - k + 1); for (let i = 0; i < n; i++) { // 移除队列中小于当前元素的元素 while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) { deque.pop(); } // 添加当前元素的索引到队列 deque.push(i); // 移除队列中不在滑动窗口中的元素 if (deque[0] < i - k + 1) { deque.shift(); } // 如果滑动窗口已经形成,将最大值添加到结果数组 if (i >= k - 1) { result[i - k + 1] = nums[deque[0]]; } } return result; }
-
5.2 代码解释
const deque = [];
: 创建一个双端队列。while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) { deque.pop(); }
: 移除队列中小于当前元素的元素。deque.push(i);
: 添加当前元素的索引到队列。if (deque[0] < i - k + 1) { deque.shift(); }
: 移除队列中不在滑动窗口中的元素。if (i >= k - 1) { result[i - k + 1] = nums[deque[0]]; }
: 如果滑动窗口已经形成,将最大值添加到结果数组。
6. 复杂度分析
-
6.1 时间复杂度
- O(n),其中 n 是数组的长度。虽然有一个
while
循环,但是每个元素最多只会被添加到队列一次,最多只会被移除队列一次。
- O(n),其中 n 是数组的长度。虽然有一个
-
6.2 空间复杂度
- O(k),需要使用一个双端队列来存储滑动窗口中的元素。
7. 总结:掌握单调队列,高效解决滑动窗口问题!
“滑动窗口最大值”是一道经典的算法题,通过学习这道题,你可以掌握单调队列这种重要的数据结构,并学会如何使用它来解决实际问题。 希望这篇博客能帮助你入门算法和数据结构,并在你的编程之路上助你一臂之力! 继续学习和实践,你一定会成为一名优秀的程序员!
力扣 239 题:滑动窗口最大值新手入门攻略
一、引言
在算法学习的道路上,滑动窗口问题是一类经典且常见的问题。力扣第 239 题“滑动窗口最大值”就是这类问题中的一个典型代表。本题要求我们在一个整数数组中,找出每个固定大小的滑动窗口内的最大值。接下来,我们将详细分析这道题,并分别给出 Java 和 JavaScript 的实现方案。
二、题目分析
2.1 题目描述
给定一个整数数组 nums
和一个大小为 k
的滑动窗口,该窗口从数组的最左侧移动到最右侧,每次只向右移动一位。我们需要返回每个滑动窗口内的最大值。
2.2 示例分析
- 示例 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]
- 输入:
2.3 提示信息
1 <= nums.length <= 10^5
-10^4 <= nums[i] <= 10^4
1 <= k <= nums.length
三、解题思路
为了高效地解决这个问题,我们可以使用单调队列。单调队列是一种特殊的数据结构,它可以在 O ( 1 ) O(1) O(1) 的时间复杂度内获取队列中的最大值。具体步骤如下:
- 初始化一个双端队列
deque
,用于存储数组元素的下标。 - 遍历数组
nums
:- 当队列不为空且队列尾部元素对应的数组值小于当前元素时,不断移除队列尾部元素,以保证队列的单调性。
- 将当前元素的下标加入队列尾部。
- 如果队列头部元素的下标已经超出了当前滑动窗口的范围,移除队列头部元素。
- 当遍历到第
k - 1
个元素及以后时,队列头部元素对应的数组值就是当前滑动窗口的最大值,将其加入结果数组。
四、Java 实现
import java.util.ArrayDeque;
import java.util.Deque;
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];
// 双端队列,存储数组元素的下标
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 移除队列尾部小于当前元素的元素
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
// 将当前元素的下标加入队列尾部
deque.offerLast(i);
// 移除队列头部超出滑动窗口范围的元素
if (deque.peekFirst() <= i - k) {
deque.pollFirst();
}
// 当遍历到第 k - 1 个元素及以后时,记录当前滑动窗口的最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
int[] result = solution.maxSlidingWindow(nums, k);
for (int num : result) {
System.out.print(num + " ");
}
}
}
4.1 代码解释
deque
是一个双端队列,用于存储数组元素的下标。- 在遍历数组时,通过
while
循环移除队列尾部小于当前元素的元素,保证队列的单调性。 - 当队列头部元素的下标超出滑动窗口范围时,使用
pollFirst
方法移除队列头部元素。 - 当遍历到第
k - 1
个元素及以后时,将队列头部元素对应的数组值加入结果数组。
4.2 复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。每个元素最多进队和出队一次。
- 空间复杂度: O ( k ) O(k) O(k),主要用于存储队列中的元素,队列中最多存储 k k k 个元素。
五、JavaScript 实现
var maxSlidingWindow = function(nums, k) {
if (nums.length === 0 || k <= 0) {
return [];
}
const n = nums.length;
const result = [];
// 双端队列,存储数组元素的下标
const deque = [];
for (let i = 0; i < n; i++) {
// 移除队列尾部小于当前元素的元素
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
// 将当前元素的下标加入队列尾部
deque.push(i);
// 移除队列头部超出滑动窗口范围的元素
if (deque[0] <= i - k) {
deque.shift();
}
// 当遍历到第 k - 1 个元素及以后时,记录当前滑动窗口的最大值
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
};
// 测试代码
const nums = [1, 3, -1, -3, 5, 3, 6, 7];
const k = 3;
const result = maxSlidingWindow(nums, k);
console.log(result);
5.1 代码解释
deque
是一个数组,用于模拟双端队列,存储数组元素的下标。- 使用
while
循环移除队列尾部小于当前元素的元素,保证队列的单调性。 - 当队列头部元素的下标超出滑动窗口范围时,使用
shift
方法移除队列头部元素。 - 当遍历到第
k - 1
个元素及以后时,将队列头部元素对应的数组值加入结果数组。
5.2 复杂度分析
- 时间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组的长度。每个元素最多进队和出队一次。
- 空间复杂度: O ( k ) O(k) O(k),主要用于存储队列中的元素,队列中最多存储 k k k 个元素。
六、总结
本题通过使用单调队列的方法,高效地解决了滑动窗口最大值的问题。在 Java 中,我们可以使用 ArrayDeque
来实现双端队列;在 JavaScript 中,我们可以使用数组来模拟双端队列。两种实现的时间复杂度和空间复杂度相同,都能满足题目的要求。希望这篇博客能帮助新手朋友们更好地理解和解决这道题目。
七、参考资料
- 力扣(LeetCode)官方网站
- 《算法导论》相关章节
【新手入门】LeetCode 239. 滑动窗口最大值:Java & JavaScript双语言详解
目录
1. 题目描述
给定一个整数数组 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]
2. 核心概念解析
什么是滑动窗口?
- 滑动窗口是一种在数组或字符串上移动的固定大小的子数组/子串。
- 每次窗口向右移动一位,直到覆盖整个数组。
如何高效求窗口最大值?
- 暴力法:每次计算窗口内的最大值,时间复杂度 O(nk),不适用于大数据量。
- 优化方法:使用双端队列(Deque),保证队列头部始终是当前窗口的最大值,时间复杂度 O(n)。
3. 解题思路分析
方法:双端队列(Deque)
- 维护一个双端队列,存储数组的索引(而不是值),队列中的索引对应的值是单调递减的。
- 遍历数组:
- 移除队列中不在当前窗口的索引(窗口左边界
i - k
)。 - 移除队列尾部比当前元素小的索引(保证队列单调递减)。
- 将当前元素索引加入队列尾部。
- 如果窗口形成(
i >= k - 1
),记录队列头部的最大值。
- 移除队列中不在当前窗口的索引(窗口左边界
- 返回结果数组。
时间复杂度: O(n)
空间复杂度: O(k)(队列最多存储 k 个元素)
4. Java实现
import java.util.ArrayDeque;
import java.util.Deque;
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];
Deque<Integer> deque = new ArrayDeque<>();
for (int i = 0; i < n; i++) {
// 移除不在窗口内的索引
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 移除队列尾部比当前元素小的索引
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i); // 加入当前索引
// 窗口形成后,记录最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
}
5. JavaScript实现
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
if (!nums.length || k <= 0) return [];
const n = nums.length;
const result = [];
const deque = []; // 存储索引
for (let i = 0; i < n; i++) {
// 移除不在窗口内的索引
while (deque.length > 0 && deque[0] < i - k + 1) {
deque.shift();
}
// 移除队列尾部比当前元素小的索引
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
deque.push(i); // 加入当前索引
// 窗口形成后,记录最大值
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
};
6. 复杂度分析
方法 | 时间复杂度 | 空间复杂度 |
---|---|---|
双端队列 | O(n) | O(k) |
暴力法 | O(nk) | O(1) |
7. 总结与扩展
- 双端队列是解决滑动窗口最大值问题的最优解,适用于大数据量。
- 扩展题目:
- 相关数据结构:
- 双端队列(Deque)
- 单调队列(Monotonic Queue)
🔗 相关推荐:LeetCode 滑动窗口专题
如果有帮助,请点赞收藏支持! 🚀
滑动窗口最大值:Java与JavaScript的高效解法
目录
- 问题描述与示例
- 暴力法与优化思路
- 单调队列法详解
- Java代码实现
- JavaScript代码实现
- 复杂度分析
- 常见问题解答
- 总结与扩展思考
1. 问题描述与示例
题目:给定一个整数数组 nums
和一个窗口大小 k
,窗口从数组最左端滑动到最右端,每次向右移动一位。返回每个窗口中的最大值。
示例:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
- 第一个窗口
[1,3,-1]
的最大值是3
。 - 第二个窗口
[3,-1,-3]
的最大值是3
。 - … 最后一个窗口
[3,6,7]
的最大值是7
。
2. 暴力法与优化思路
暴力法
思路:
- 对每个窗口遍历
k
个元素,找到最大值。
步骤:
- 遍历每个起始索引
i
,窗口范围为[i, i+k-1]
。 - 在窗口内遍历元素,记录最大值。
问题:
- 时间复杂度为 O(nk),当
n=1e5
时会超时。
3. 单调队列法详解
核心思想
使用单调队列维护窗口内可能成为最大值的元素:
- 队列中的元素按递减顺序排列,队头始终是当前窗口的最大值。
- 队列中存储元素的索引,方便判断是否过期。
步骤:
- 初始化双端队列:保存元素的索引。
- 遍历数组:
- 移除过期元素:队头索引是否已滑出窗口(
<= i-k
)。 - 维护单调性:弹出队尾所有比当前元素小的索引。
- 加入当前元素:将当前索引加入队尾。
- 记录结果:当窗口形成后(
i >=k-1
),队头即为最大值。
- 移除过期元素:队头索引是否已滑出窗口(
4. Java代码实现
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.ArrayList;
public class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums == null || nums.length == 0) return new int[0];
Deque<Integer> deque = new ArrayDeque<>();
List<Integer> result = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
// 移除过期的头部元素
while (!deque.isEmpty() && deque.peek() <= i - k) {
deque.poll();
}
// 维护单调性:弹出队尾所有小于当前元素的索引
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast();
}
deque.offer(i);
// 窗口形成后记录最大值
if (i >= k - 1) {
result.add(nums[deque.peek()]);
}
}
// 转换为数组
int[] res = new int[result.size()];
for (int j = 0; j < res.length; j++) {
res[j] = result.get(j);
}
return res;
}
}
关键点:
- 双端队列:
ArrayDeque
实现高效插入和删除。 - 索引过期判断:队头索引是否已滑出窗口。
- 单调性维护:确保队列始终递减。
5. JavaScript代码实现
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function(nums, k) {
const deque = []; // 存储索引
const result = [];
for (let i = 0; i < nums.length; i++) {
// 维护单调性:弹出队尾所有小于当前元素的索引
while (deque.length > 0 && nums[i] >= nums[deque[deque.length - 1]]) {
deque.pop();
}
deque.push(i);
// 处理过期的头部元素(在记录结果时检查)
if (i >= k - 1) {
// 移除过期的头部元素
while (deque[0] <= i - k) {
deque.shift();
}
result.push(nums[deque[0]]);
}
}
return result;
};
关键点:
- 数组模拟双端队列:用
push
和pop
操作维护队尾,用shift
处理队头。 - 过期检查:在记录结果时确保队头未过期。
6. 复杂度分析
时间复杂度
- O(n):每个元素最多被加入和弹出队列一次。
空间复杂度
- O(k):队列中最多存储
k
个元素(窗口内元素)。
7. 常见问题解答
Q1:为什么队列要保存索引而不是数值?
- 索引可以方便判断元素是否过期(是否滑出窗口)。
Q2:为什么队列需要保持递减?
- 递减队列确保队头始终是当前窗口的最大值。
Q3:JavaScript 的 shift()
操作会不会影响性能?
- 每个元素最多被
shift
一次,总时间复杂度仍为 O(n)。
8. 总结与扩展思考
总结
- 单调队列法 是解决滑动窗口问题的高效工具,时间复杂度为 O(n)。
- 核心是通过队列的单调性快速定位最大值,并通过索引管理过期元素。
进阶思考
- 其他窗口问题:如最小值、均值等,可类似思路优化。
- 动态窗口问题:如最长无重复子串,可结合滑动窗口与哈希表。
推荐阅读
- LeetCode 题目:239. 滑动窗口最大值
- 算法学习:单调队列在动态规划中的应用(如最长递增子序列)。
通过本篇博客,你掌握了如何用单调队列高效解决滑动窗口最大值问题。无论是 Java 还是 JavaScript,核心逻辑一致,仅需调整语法细节即可。
LeetCode 239. 滑动窗口最大值:Java与JavaScript双解法详解|单调队列的精妙运用
目录
一、问题描述
给定一个整数数组 nums
和滑动窗口的大小 k
,窗口从数组最左侧移动到最右侧,每次移动一位。要求返回每个窗口中的最大值。
示例:
输入: nums = [1,3,-1,-3,5,3,6,7], k = 3
输出: [3,3,5,5,6,7]
关键要求:
- 时间复杂度必须为 O(n)
- 数组长度可达 10^5
- 需要处理 k=1 的特殊情况
二、核心思路:单调队列的智慧
暴力法 vs 单调队列对比
方法 | 核心思路 | 时间复杂度 | 空间复杂度 |
---|---|---|---|
暴力法 | 每个窗口重新扫描找最大值 | O(nk) | O(1) |
单调队列 | 动态维护递减队列获取最大值 | O(n) | O(k) |
算法步骤
- 维护递减队列:队列头始终是当前窗口最大值
- 窗口滑动操作:
- 移除窗口外元素(索引检查)
- 维护队列递减性(弹出比新元素小的尾部元素)
- 记录结果:当窗口完全形成后记录队列头元素
三、Java解法详解
完整代码实现
import java.util.Deque;
import java.util.LinkedList;
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
if (nums.length == 0 || k == 0) return new int[0];
Deque<Integer> deque = new LinkedList<>();
int[] result = new int[nums.length - k + 1];
for (int i = 0; i < nums.length; i++) {
// 移除窗口外的元素
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 维护递减队列
while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()]) {
deque.pollLast();
}
deque.offerLast(i);
// 记录窗口最大值
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
}
关键代码解析
- 窗口外元素检查:
deque.peekFirst() < i - k + 1 // 检查队列头是否在窗口左侧之外
- 递减性维护逻辑:
nums[i] >= nums[deque.peekLast()] // 新元素比队尾大就弹出队尾
- 结果记录时机:
i >= k - 1 // 当窗口完全进入数组后开始记录
测试案例
public static void main(String[] args) {
Solution sol = new Solution();
int[] test1 = {1,3,-1,-3,5,3,6,7};
System.out.println(Arrays.toString(sol.maxSlidingWindow(test1, 3)));
// 输出 [3,3,5,5,6,7]
}
四、JavaScript解法详解
完整代码实现
const maxSlidingWindow = (nums, k) => {
if (nums.length === 0 || k === 0) return [];
const deque = [];
const result = [];
for (let i = 0; i < nums.length; i++) {
// 移除窗口外的元素
while (deque.length > 0 && deque[0] < i - k + 1) {
deque.shift();
}
// 维护递减队列
while (deque.length > 0 && nums[i] >= nums[deque[deque.length - 1]]) {
deque.pop();
}
deque.push(i);
// 记录窗口最大值
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
};
关键代码解析
- 数组模拟双端队列:
deque.shift() // 移除队头 deque.pop() // 移除队尾 deque.push() // 加入队尾
- 索引存储技巧:
deque存储的是元素索引而非值,方便进行窗口位置检查
浏览器测试
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3));
// 输出 [3,3,5,5,6,7]
五、复杂度分析
时间复杂度:O(n)
- 每个元素最多入队、出队各一次
- 即便使用JavaScript的
shift()
,整体操作次数仍为O(n)
空间复杂度:O(k)
- 队列最多存储k个元素(严格递减时)
六、Java与JavaScript实现对比
特性 | Java | JavaScript |
---|---|---|
队列实现 | 标准库Deque | 数组模拟 |
元素移除效率 | O(1)(链表实现) | O(1)平均(引擎优化) |
空数组处理 | 显式检查返回空数组 | 同左 |
索引存储 | 必须存储 | 必须存储 |
七、常见错误与解决方法
错误1:直接存储数值而非索引
// 错误示例:无法进行窗口位置检查
deque.add(nums[i]);
解决方法:必须存储元素索引
错误2:忘记处理k=0的边界条件
// 错误:当k=0时会导致死循环
if (k === 0) return [];
错误3:错误计算结果索引
// 错误:结果数组索引计算错误
result[i] = ... // 正确应为 result[i - k + 1]
八、举一反三练习
- 变形题:剑指 Offer 59 - II. 队列的最大值(实现带max函数的队列)
- 进阶题:862. 和至少为 K 的最短子数组(前缀和+单调队列)
- 系统设计:如何实时显示股票交易窗口的最高价?
学习建议:
尝试使用其他数据结构(如优先队列)解决此题,比较不同方法的性能差异。
掌握单调队列的核心在于理解其动态维护最值的能力。这种数据结构在滑动窗口类问题中展现出惊人的效率,是算法工程师必须掌握的利器。下次遇到需要动态获取窗口极值的问题时,记得祭出这个法宝! 🔥
力扣239题详解:滑动窗口最大值(Java & JavaScript 双语言实现)
目录
1. 问题描述 {#1}
给定一个整数数组 nums
和一个整数 k
,滑动窗口从数组最左侧移动到最右侧,每次窗口向右滑动一位。返回每次滑动窗口中的最大值。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
2. 核心思路 {#2}
单调队列(双端队列):
- 目标:维护一个队列,存储窗口内可能成为最大值的元素下标,保证队列元素严格单调递减。
- 关键操作:
- 移除窗口外元素:当队首元素超出窗口左边界时弹出。
- 维护单调性:新元素入队前,弹出队尾比它小的元素。
- 时间复杂度:O(n),每个元素最多入队和出队一次。
3. 解法:单调队列 {#3}
Java 实现
import java.util.Deque;
import java.util.LinkedList;
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
Deque<Integer> deque = new LinkedList<>();
int[] res = new int[nums.length - k + 1];
int idx = 0;
for (int i = 0; i < nums.length; i++) {
// 移除窗口外的队首元素
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// 维护单调递减性
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
// 窗口形成后记录最大值
if (i >= k - 1) {
res[idx++] = nums[deque.peekFirst()];
}
}
return res;
}
}
JavaScript 实现
function maxSlidingWindow(nums, k) {
const deque = [];
const res = [];
for (let i = 0; i < nums.length; i++) {
// 移除窗口外的队首元素
while (deque.length > 0 && deque < i - k + 1) {
deque.shift();
}
// 维护单调递减性
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
deque.push(i);
// 窗口形成后记录最大值
if (i >= k - 1) {
res.push(nums[deque]);
}
}
return res;
}
4. 复杂度分析 {#4}
操作 | 时间复杂度 | 空间复杂度 |
---|---|---|
遍历数组 | O(n) | O(k) |
Java | 双端队列操作 O(1) | 结果数组 O(n) |
JavaScript | 数组模拟队列 O(n) | 结果数组 O(n) |
5. 测试用例 {#6}
Java 测试:
public static void main(String[] args) {
Solution solution = new Solution();
int[] nums = {1,3,-1,-3,5,3,6,7};
int[] result = solution.maxSlidingWindow(nums, 3);
System.out.println(Arrays.toString(result)); // [3,3,5,5,6,7]
}
JavaScript 测试:
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3)); // [3,3,5,5,6,7]
7. 总结 {#7}
- 单调队列:通过动态维护递减队列,高效获取窗口最大值,时间复杂度优化至 O(n)。
- 语言差异:Java 使用
Deque
双端队列,JavaScript 需用数组手动维护(可优化为指针避免shift
的低效操作)。 - 适用场景:实时获取动态窗口内的极值,如股票分析、实时监控等高频数据处理场景。
参考资料
单调队列原理与实现逻辑
双端队列操作与复杂度分析