之前我们讲了 队列、栈 优先队列,本文看下单调队列和单调栈。
单调队列
也许这种数据结构的你没听过,但他其实就是一个队列,只不过是用了巧妙的方法使得队列中的元素单调递增或单调递减。(当然,优先队列也可以求最值,只不过 JavaScript 语言没有原生支持,实现起来较麻烦)
单调队列解决什么问题?
给出数字数组 arr,比如他有 5 个元素,这时可能有 2 个操作:
- 插入新元素
- 删除已有元素
这两个操作后,你需要获得最值,并且时间复杂度保持在 O(1)
LeetCode 题目实践
当移到 k 时,也就是上面说的问题:
把-3 加入数组,并且把 1 移出数组,并且获取最大值,
单调队列实现
正常队列是这样:
class Queue {
private queue: number[] = [];
push(n: number) {
this.queue.push(n);
}
pop(n: number) {
this.queue.shift();
}
}
单调队列需要在 push 时特殊处理,以保持他的单调性,这里我们实现单调递增队列:
push(n: number) {
// 如果加入的元素 n 大于队尾的元素,那么从队尾删除掉元素
while (n > this.queue[this.queue.length - 1]) {
this.queue.pop();
}
this.queue.push(n);
}
这里比 n 小的元素没有加入队列,所以队头 pop 时也不能全都 pop 出去,应该判断下,只有是最大值时,才真正从队头删除
pop(n: number) {
if (this.queue[0] === n) {
this.queue.shift();
}
}
解题代码
class MonotonicQueue {
private queue: number[] = [];
push(n: number) {
// 如果加入的元素 n 大于队尾的元素,那么从队尾删除掉元素
while (n > this.queue[this.queue.length - 1]) {
this.queue.pop();
}
this.queue.push(n);
}
max(): number {
return this.queue[0];
}
pop(n: number) {
if (this.queue[0] === n) {
this.queue.shift();
}
}
}
function maxSlidingWindow(nums: number[], k: number): number[] {
let res: number[] = [];
let mq = new MonotonicQueue();
for (let i = 0; i < nums.length; i++) {
if (i < k) {
mq.push(nums[i]);
} else {
res.push(mq.max());
mq.push(nums[i]);
mq.pop(nums[i - k]);
}
}
res.push(mq.max());
return res;
}
单调栈
也是和单调队列类似,就是一个栈结构,但是符合单调递增或单调递减,他解决的问题是离当前元素最近的比当前元素大的值或小的值,来看题目:
这是典型的单调栈问题,只要求出 nums2 的单调栈即可
function nextGreaterElement(nums1: number[], nums2: number[]): number[] {
const stack:number[] = []
const res = new Map<number,number>()
for(let i=nums2.length-1;i>=0;i--){
while(stack.length&&stack[stack.length-1]<nums2[i]) {
stack.pop()
}
if (stack.length) {
res.set(nums2[i],stack[stack.length-1])
} else {
res.set(nums2[i],-1)
}
stack.push(nums2[i])
}
const show:number[] = []
nums1.forEach(i => {
show.push(res.get(i))
})
return show
};
单调栈结果存储在 res 中
蓝色框框是单调栈的通用代码,他和单调队列很像:如果栈顶的元素小于要加入的值,那就把栈顶元素拿出去,直到栈顶元素大于等于要加入的值了,再把当前元素入栈,这样就保证了栈是单调递减的。
红色框框是获取结果的逻辑,即在要入栈之前,找到栈顶元素,也就是最近一个大于等于当前元素的值,也就是题目的答案,即「下一个更大元素」
别看 for 里面套了 while 循环就以为是 O(n^2)
时间复杂度,实际上是 O(n)
,因为每个元素最多被 push 或 pop 1 次。
再看一道题目
这题还是典型的单调栈题目,只不过加大了点难度,说是这个数组是循环的,其实仔细想下:只要把数组再复制一份,加到后面即可,一般循环相关题目,都可以先考虑复制一份的思路。
function nextGreaterElements(nums: number[]): number[] {
let len = nums.length
nums = [...nums,...nums]
let stack: number[] = []
let res:number[] =[]
for(let i=nums.length-1;i>=0;i--){
// 用例中有等于的情况,所以要使用 <=
while(stack.length&&stack[stack.length-1]<=nums[i]){
stack.pop()
}
if(stack.length){
res[i]=stack[stack.length-1]
}else{
res[i]=-1
}
stack.push(nums[i])
}
return res.slice(0,len)
};