一、前置知识(可跳过)
什么是双端队列(Deque)?
双端队列(Double-Ended Queue,简称 Deque)是一种允许在两端进行插入和删除操作的线性数据结构。它结合了队列和栈的特性。
Deque 的特点:
- 双向操作: 可以在队列的头部和尾部进行添加和删除操作。
- 灵活性: 可以用作队列(FIFO)或栈(LIFO)。
Deque 的基本操作:
push_back(element)
或push(element)
: 在队列尾部添加一个元素。push_front(element)
或unshift(element)
: 在队列头部添加一个元素。pop_back()
或pop()
: 从队列尾部删除一个元素。pop_front()
或shift()
: 从队列头部删除一个元素。peek_front()
或front()
: 查看队列头部的元素(不删除)。peek_back()
或back()
: 查看队列尾部的元素(不删除)。isEmpty()
: 检查队列是否为空。size()
: 返回队列中元素的数量。
二、题目描述------滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
**的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
ini
输入: 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:
ini
输入: nums = [1], k = 1
输出: [1]
提示:
1 <= nums.length <= 105
-104 <= nums[i] <= 104
1 <= k <= nums.length
三、题解
超时题解(本人)
js
var maxSlidingWindow = function(nums, k) {
let left=0;
let right=k-1;
let ans=nums[0];
let anss=[];
if(k===0||nums.length===0) return [];
if(nums.length===1) return [nums[0]];
if (k===1){
for(let i=0;i<nums.length;i++){
anss.push(nums[i]);
}
return anss;
}
for(let i=0;i<k;i++){
ans=Math.max(ans,nums[i]);
}
anss.push(ans);
left++;
right++;
while(right<nums.length){
if(nums[left-1]===ans){
ans=nums[left];
for(let i=left;i<=right;i++){
ans=Math.max(ans,nums[i]);
}
anss.push(ans);
left++;
right++;
}else{
ans=Math.max(ans,nums[right]);
anss.push(ans);
left++;
right++;
}
}
return anss;
};
该题解时间复杂度为O(KN)超时了,便不再赘述
正解(单调队列)
js
/**
* @param {number[]} nums // 输入:一个数字数组 nums
* @param {number} k // 输入:滑动窗口的大小 k
* @return {number[]} // 输出:一个数组,包含每次滑动窗口的最大值
*/
var maxSlidingWindow = function(nums, k) {
const n = nums.length; // 获取数组 nums 的长度
// 边界情况处理:
if (n === 0 || k === 0) return []; // 如果数组为空或者窗口大小为 0,则返回空数组
if (k === 1) return nums; // 如果窗口大小为 1,则直接返回原数组(每个元素都是当前窗口的最大值)
const result = []; // 初始化结果数组,用于存储每个窗口的最大值
const deque = []; // 初始化双端队列 (deque),用于存储窗口内元素的索引。 这个队列是单调递减的。
// 主循环:遍历数组 nums
for (let i = 0; i < n; i++) {
// 1. 移除超出窗口范围的元素:
// 循环条件:
// - deque 数组不为空 (deque.length > 0)
// - 队列头部的索引小于等于 i - k (deque[0] <= i - k)。 这意味着队列头部的元素已经滑出了当前窗口。
while (deque.length > 0 && deque[0] <= i - k) {
deque.shift(); // 从队列头部移除元素! 这保证了队列中只包含当前窗口内的元素的索引。
}
// 2. 维护队列的单调递减性:
// 循环条件:
// - deque 数组不为空 (deque.length > 0)
// - 队列尾部索引对应的元素值小于等于当前元素值 (nums[deque[deque.length - 1]] <= nums[i]) 。这意味着当前元素比队列尾部的元素更大或相等,那么队列尾部的元素就不可能是窗口的最大值了,所以需要移除。
while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i]) {
deque.pop(); // 从队列尾部移除元素! 这保证了队列中的元素是单调递减的,队首元素始终是当前窗口的最大值。
}
// 3. 将当前元素的索引添加到队列尾部:
deque.push(i); // 将当前遍历到的元素的索引 i 添加到队列尾部。
// 4. 当窗口完全形成时,将最大值添加到结果数组:
// 条件:i >= k - 1 表示当前窗口已经完全形成,可以计算最大值了。 例如,当k为3时,i=2时,result开始push
if (i >= k - 1) {
result.push(nums[deque[0]]); // 将队列头部索引对应的元素值(当前窗口的最大值)添加到 result 数组中。 deque[0] 始终是队列中最大的元素的索引。
}
}
return result; // 返回结果数组
};
核心思想
该解法的核心是使用单调递减队列(deque)来高效地维护滑动窗口中的最大值。 单调队列保证队首元素始终是当前窗口的最大值,从而避免了每次滑动窗口时都进行线性扫描。
详细解析
-
初始化:
nums
:输入数组.k
:窗口大小.result
:存储结果的数组.deque
:双端队列,存储 索引 , 保持队列内的元素 从大到小递减 (单调递减).
-
遍历数组:
for (let i = 0; i < n; i++)
循环遍历nums
数组。-
维护队列:
-
移除过期元素:
while (deque.length > 0 && deque[0] <= i - k)
- 如果队列头的索引已经超出窗口范围
i - k
,则从队列头移除该索引。 目的是保持队列中的索引都在当前窗口内。
- 如果队列头的索引已经超出窗口范围
-
保持单调递减:
while (deque.length > 0 && nums[deque[deque.length - 1]] <= nums[i])
- 如果当前元素
nums[i]
大于等于队列尾部索引对应的元素值nums[deque[deque.length - 1]]
,则从队列尾部移除索引,直到队列为空或者找到一个比当前元素大的元素。 目的是确保队列是单调递减的,这样队列头部始终是当前窗口的最大值索引。
- 如果当前元素
-
添加当前元素索引:
deque.push(i)
- 将当前元素的索引
i
添加到队列尾部。
- 将当前元素的索引
-
-
添加到结果集:
if (i >= k - 1)
- 当窗口完全形成(
i >= k - 1
)之后,将队列头部索引 correspond的值nums[deque[0]]
(当前窗口的最大值) 添加到结果数组result
中。
- 当窗口完全形成(
-
-
返回结果:
return result
四、结语
再见!