Skip to content

Commit

Permalink
monoQueue
Browse files Browse the repository at this point in the history
  • Loading branch information
yk committed Feb 27, 2024
1 parent e57aadc commit 1504291
Showing 1 changed file with 69 additions and 81 deletions.
150 changes: 69 additions & 81 deletions content/posts/algorithm/data structure/单调队列.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,34 @@
---
title: '单调队列'
date: 2023-01-31T09:15:05+08:00
lassmod: 2024-02-27
series: [data structure]
categories: [algorithm]
---

栈有单调栈,队列自然也有单调队列,性质也是一样的,保持队列内的元素有序,单调递增或递减。
### 概念

其实动态求极值首先可以联想到的应该是优先队列,但是,优先队列无法满足「先进先出」的时间顺序,所以单调队列应运而生,一般可以用来解决类似求滑动窗口中极值的问题
栈有单调栈,队列自然也有单调队列,性质也是一样的,保持队列内的元素有序,单调递增或递减

### JS 的单调队列实现
其实动态求极值首先可以联想到的应该是 「优先队列」,但是,优先队列无法满足**「先进先出」**的时间顺序,所以单调队列应运而生。

```js
class MonoQueue {
constructor() {
this.queue = []
}
// 重点就是在入队时保证单调性,与单调栈一样,挤压式入队~
push(num) {
const q = this.queue
while (q.length && q[q.length - 1] < num) {
q.pop()
}
q.push(num)
}
pop(num) {
const q = this.queue
if (num === q[q.length - 1]) this.queue.shift()
}
peek() {
return this.queue[0]
}
// 关键点, 保持单调性,其拍平效果与单调栈一致
while (q.length && num (<= | >=) q[q.length - 1]) {
q.pop()
}
q.push(num)
```

### lc.918 环形子数组的最大和(单调队列)
## 场景

给你一个数组 window,已知其最值为 A,如果给 window 中添加一个数 B,那么比较一下 A 和 B 就可以立即算出新的最值;但如果要从 window 数组中减少一个数,就不能直接得到最值了,因为如果减少的这个数恰好是 A,就需要遍历 window 中的所有元素重新寻找新的最值。

### [239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/)
## 练一练

滑动窗口嘛,完美符合一边进,一边出的条件,所以用单调队列来实现这个滑动窗口再好不过了。
### lc239. 滑动窗口最大值 hard

动态计算极值,直接命中单调队列的使用条件。

```js
/**
Expand All @@ -47,83 +37,81 @@ class MonoQueue {
* @return {number[]}
*/
var maxSlidingWindow = function (nums, k) {
// 单调队列window, 当这个窗口内保持单调递减,
// 也就是每次新进数据如果够大就把阻拦在其之前的数据拍掉,与单调栈类似
const monoWindow = []
const res = []
let res = []
const monoQueue = []
for (let i = 0; i < nums.length; ++i) {
while (monoWindow.length && nums[i] >= nums[monoWindow[monoWindow.length - 1]]) {
monoWindow.pop()
while (monoQueue.length && nums[i] >= nums[monoQueue[monoQueue.length - 1]]) {
monoQueue.pop()
}
monoWindow.push(i) // 存的是索引
// 根据区间 [l..r] 的长度 与 k 的关系来判断是否需要退出队首
if (i - monoWindow[0] + 1 > k) monoWindow.shift()
/** 一个重点是存储索引,便于判断窗口的大小 */
monoQueue.push(i)
if (i - monoQueue[0] + 1 > k) monoQueue.shift()

// r - l + 1 == k 所以 l = r - k + 1 保证有意义
if (i - k + 1 >= 0) {
res[i - k + 1] = nums[monoWindow[0]]
}
if (i - k + 1 >= 0) res.push(nums[monoQueue[0]])
}
return res
}
```

当然啦,如果直接用自己实现的单调队列数据结构来处理的话,就更好理解了。
### lc.862 和至少为 K 的最短子数组 hard

看题目就知道,离不开前缀和。想到单调队列,是有难度的,起码我一开始想不到 😭

<!-- copy了答案,后续再细看 -->

```js
var maxSlidingWindow = function (nums, k) {
const res = []
const monoQueue = new MonoQueue() // 见上方
for (let i = 0; i < nums.length; ++i) {
if (i < k - 1) {
monoQueue.push(nums[i])
} else {
monoQueue.push(nums[i])
res.push(monoQueue.peek())
monoQueue.pop(nums[i - k + 1])
var shortestSubarray = function (nums, k) {
const n = nums.length
const preSumArr = new Array(n + 1).fill(0)
for (let i = 0; i < n; i++) {
preSumArr[i + 1] = preSumArr[i] + nums[i]
}
let res = n + 1
const queue = []
for (let i = 0; i <= n; i++) {
const curSum = preSumArr[i]
while (queue.length != 0 && curSum - preSumArr[queue[0]] >= k) {
res = Math.min(res, i - queue.shift())
}
while (queue.length != 0 && preSumArr[queue[queue.length - 1]] >= curSum) {
queue.pop()
}
queue.push(i)
}
return res
return res < n + 1 ? res : -1
}
```

### [剑指 offer 59 - II. 队列的最大值](https://leetcode.cn/problems/dui-lie-de-zui-da-zhi-lcof/)

这道题,与上方我们自己实现的单调队列唯一不同的是,我们的单调队列 pop 可以传参,来确定被 pop 的是不是此时应该出队的元素,此题的 pop 没有给传参的机会,怎么做呢?
### lc.918 环形子数组的最大和(单调队列)

真相只有一个:双队列,用一个普通队列来记录所有元素,另一个队列作为单调队列即可。
这道题在前缀和的时候遇到过,再来复习一次吧 😁

```js
var MaxQueue = function () {
this.queue = [] // 借助它来帮助 monoQueue 的 pop 实现
this.monoQueue = []
}

/**
* @param {number[]} nums
* @return {number}
*/
MaxQueue.prototype.max_value = function () {
return this.monoQueue.length ? this.monoQueue[0] : -1
}

/**
* @param {number} value
* @return {void}
*/
MaxQueue.prototype.push_back = function (value) {
this.queue.push(value)
while (this.monoQueue.length && value > this.monoQueue[this.monoQueue.length - 1]) {
this.monoQueue.pop()
var maxSubarraySumCircular = function (nums) {
// 这道题是比较综合的一道题,用到了前缀和,循环数组技巧,滑动窗口和单调队列技巧
let max = -Infinity
const monoQueue = [[0, nums[0]]] // 单调队列存储 【index,preSum】的数据结构
const n = nums.length
let preSum = nums[0]
for (let i = 1; i < 2 * n; ++i) {
preSum += nums[i % n]
max = Math.max(max, preSum - monoQueue[0][1]) // 子数组和越大,减去的就应该越小
while (monoQueue.length && preSum <= monoQueue[monoQueue.length - 1][1]) {
monoQueue.pop()
}
monoQueue.push([i, preSum])
// 根据索引控制 窗口大小
while (monoQueue.length && i - monoQueue[0][0] + 1 > n) {
monoQueue.shift()
}
}
this.monoQueue.push(value)
}

/**
* @return {number}
*/
MaxQueue.prototype.pop_front = function () {
if (!this.queue.length) return -1 // 题目要求 与单调队列核心无关
const shiftEl = this.queue.shift()
if (shiftEl === this.monoQueue[0]) this.monoQueue.shift()
return shiftEl // 题目要求 与单调队列核心无关
return max
}
```

<!-- lc.1425 带限制的子序列和 hard; lc.1696 跳跃游戏 VI 有时间再做做吧-->

0 comments on commit 1504291

Please sign in to comment.