双端队列与单调队列:滑动窗口最值

前言

双端队列是一种特殊的队列,它支持从两端进行元素的插入和删除操作。与普通队列不同,双端队列可以在队列的头部和尾部插入元素,也可以从头部和尾部删除元素。这种灵活性使得双端队列在一些特定的场景中非常有用,例如需要在队列和栈之间切换操作的情况,或者需要在任意一端高效地进行插入和删除操作的情况。

单调队列是一种数据具有单调性的队列。单调队列可以优化许多算法,例如滑动区间最值的计算。在单调队列中,元素按照特定的顺序排列,通常是单调递增或单调递减的。这种排序性质使得单调队列可以快速地找到队列中的最大值或最小值,从而提高算法的效率。

正文

题目

给你一个整数数组 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

双指针暴力解(超时)

思路

大体思路:模拟滑动窗口移动过程,并且通过循环在滑动窗口内遍历寻找最大值。

详细实现:

  1. 通过设置起始值为0k-1的双指针在原数组上模拟滑动窗口的初始位置。创建一个结果数组(arr)存放每次窗口滑动后窗口内的最大值。
  2. 通过while循环到滑动窗口的右边界和原数组的右边界重合时结束循环。
  3. 在while循环中,通过for循环在原数组的双指针区域(滑动窗口区域)内依次比较,得出最大值,然后将最大值放入结果数组(arr)中。
  4. 在while循环中,for循环结束后进行双指针的加一操作,实现滑动窗口的移动。
  5. 在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),结果超出了时间限制。

单调队列

思路

滑动窗口的移动每次从左边界出一个元素,从右边界进一个元素。到这有没有感觉到一股熟悉的味道呢?没错就是先进先出,这让我们马上联想到队列。

大体思路:通过队列的一次出队和入队模拟滑动窗口的移动;保持队列是单调队列,在单调队列可以快速地找到队列中的最大值。

具体实现:

  1. 创建一个结果数组(arr)存放最大值,创建一个双端队列。

  2. 对原数组进行for循环遍历:

    1. 通过一个while循环实现将队列中比即将入队的元素小或相同大小的元素出栈。

      在队列不为空并且要push入队列的原数组元素大于或等于对列的末尾元素时,while循环才能继续循环,在while循环中进行队列元素pop操作。

      图解

  1. 第一个while循环执行结束后将遍历到的原数组及其下标一起放入队列中。eg: queue.push([nums[i], i])。为什么要这样做,后面就知道了。

  2. 再通过一个while循环检查队列头部元素的索引是否超出了滑动窗口的左边界(i - k),如果超出则将其shift出队列。在这就揭秘了为什么要保留元素的下标索引。

  3. 对当前索引进行判断,当索引大于等于 k - 1 时(也就是到达了滑动窗口大小时),将队列头部元素(即当前滑动窗口内的最大值)添加到结果数组(arr)中。

  4. 返回结果数组(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
};

小结

在一些场景中,找到该场景的特点后再联想是否有符合该特点的数据结构,这样既能解决问题又能巩固各数据结构的特点。

相关推荐
tuokuac7 分钟前
nginx配置前端请求转发到指定的后端ip
前端·tcp/ip·nginx
程序员爱钓鱼11 分钟前
Go语言实战案例-开发一个Markdown转HTML工具
前端·后端·go
万少33 分钟前
鸿蒙创新赛 HarmonyOS 6.0.0(20) 关键特性汇总
前端
桦说编程39 分钟前
爆赞!完全认同!《软件设计的哲学》这本书深得我心
后端
thinktik1 小时前
还在手把手教AI写代码么? 让你的AWS Kiro AI IDE直接读飞书需求文档给你打工吧!
后端·serverless·aws
还有多远.1 小时前
jsBridge接入流程
前端·javascript·vue.js·react.js
蝶恋舞者1 小时前
web 网页数据传输处理过程
前端
非凡ghost1 小时前
FxSound:提升音频体验,让音乐更动听
前端·学习·音视频·生活·软件需求
w2sfot1 小时前
Passing Arguments as an Object in JavaScript
开发语言·javascript·ecmascript