前言
双端队列是一种特殊的队列,它支持从两端进行元素的插入和删除操作。与普通队列不同,双端队列可以在队列的头部和尾部插入元素,也可以从头部和尾部删除元素。这种灵活性使得双端队列在一些特定的场景中非常有用,例如需要在队列和栈之间切换操作的情况,或者需要在任意一端高效地进行插入和删除操作的情况。
单调队列是一种数据具有单调性的队列。单调队列可以优化许多算法,例如滑动区间最值的计算。在单调队列中,元素按照特定的顺序排列,通常是单调递增或单调递减的。这种排序性质使得单调队列可以快速地找到队列中的最大值或最小值,从而提高算法的效率。
正文
题目
给你一个整数数组 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
双指针暴力解(超时)
思路
大体思路:模拟滑动窗口移动过程,并且通过循环在滑动窗口内遍历寻找最大值。
详细实现:
- 通过设置起始值为
0
和k-1
的双指针在原数组上模拟滑动窗口的初始位置。创建一个结果数组(arr)存放每次窗口滑动后窗口内的最大值。 - 通过while循环到滑动窗口的右边界和原数组的右边界重合时结束循环。
- 在while循环中,通过for循环在原数组的双指针区域(滑动窗口区域)内依次比较,得出最大值,然后将最大值放入结果数组(arr)中。
- 在while循环中,for循环结束后进行双指针的加一操作,实现滑动窗口的移动。
- 在while循环结束后返回结果数组(arr)。
代码实现
javascript
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
var a = 0;
var b = k - 1;
var arr = []
while (b < nums.length) {
var max = -Infinity
for (var i = a; i <= b; i++) {
if (max < nums[i])
max = nums[i];
}
arr.push(max)
a++
b++
}
return arr;
};
但是时间复杂度为O(n*k),结果超出了时间限制。
单调队列
思路
滑动窗口的移动每次从左边界出一个元素,从右边界进一个元素。到这有没有感觉到一股熟悉的味道呢?没错就是先进先出,这让我们马上联想到队列。
大体思路:通过队列的一次出队和入队模拟滑动窗口的移动;保持队列是单调队列,在单调队列可以快速地找到队列中的最大值。
具体实现:
-
创建一个结果数组(arr)存放最大值,创建一个双端队列。
-
对原数组进行for循环遍历:
-
通过一个while循环实现将队列中比即将入队的元素小或相同大小的元素出栈。
在队列不为空并且要push入队列的原数组元素大于或等于对列的末尾元素时,while循环才能继续循环,在while循环中进行队列元素pop操作。
图解:
-
-
第一个while循环执行结束后将遍历到的原数组及其下标一起放入队列中。eg:
queue.push([nums[i], i])
。为什么要这样做,后面就知道了。 -
再通过一个while循环检查队列头部元素的索引是否超出了滑动窗口的左边界(i - k),如果超出则将其shift出队列。在这就揭秘了为什么要保留元素的下标索引。
-
对当前索引进行判断,当索引大于等于 k - 1 时(也就是到达了滑动窗口大小时),将队列头部元素(即当前滑动窗口内的最大值)添加到结果数组(arr)中。
-
返回结果数组(arr)。
举例图解:
eg:nums = [1, 3, -1, -3, 5, 3, 6, 7]
并且k = 3
代码实现
JavaScript
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
var arr = []
var queue = []
for (let i = 0; i < nums.length; i++) {
while (queue.length && nums[i] >= queue[queue.length - 1][0]) {
queue.pop()
}
queue.push([nums[i], i])
while (queue.length && queue[0][1] <= i - k) {
queue.shift()
}
if (i >= k - 1) {
arr.push(queue[0][0])
}
}
return arr
};
代码优化
消耗内存有点大,我们可以优化代码,减少内存消耗。
可以将原本存储的元素值及其下标的方法改为只存储元素下标。
JavaScript
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
var arr = []
var queue = []
for (let i = 0; i < nums.length; i++) {
while (queue.length && nums[i] >= nums[queue[queue.length - 1]]) {
queue.pop()
}
queue.push(i)
while (queue.length && queue[0] <= i - k) {
queue.shift()
}
if (i >= k - 1) {
arr.push(nums[queue[0]])
}
}
return arr
};
小结
在一些场景中,找到该场景的特点后再联想是否有符合该特点的数据结构,这样既能解决问题又能巩固各数据结构的特点。