之前我们讲了 队列、栈 优先队列,本文看下单调队列和单调栈。

单调队列

也许这种数据结构的你没听过,但他其实就是一个队列,只不过是用了巧妙的方法使得队列中的元素单调递增或单调递减。(当然,优先队列也可以求最值,只不过 JavaScript 语言没有原生支持,实现起来较麻烦)

单调队列解决什么问题?

给出数字数组 arr,比如他有 5 个元素,这时可能有 2 个操作:

  1. 插入新元素
  2. 删除已有元素

这两个操作后,你需要获得最值,并且时间复杂度保持在 O(1)

LeetCode 题目实践

239.  滑动窗口最大值

image.png

当移到 k 时,也就是上面说的问题:

image.png

把-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;
}

单调栈

也是和单调队列类似,就是一个栈结构,但是符合单调递增或单调递减,他解决的问题是离当前元素最近的比当前元素大的值或小的值,来看题目:

496.  下一个更大元素 I

image.png

这是典型的单调栈问题,只要求出 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 中

image.png

蓝色框框是单调栈的通用代码,他和单调队列很像:如果栈顶的元素小于要加入的值,那就把栈顶元素拿出去,直到栈顶元素大于等于要加入的值了,再把当前元素入栈,这样就保证了栈是单调递减的。

红色框框是获取结果的逻辑,即在要入栈之前,找到栈顶元素,也就是最近一个大于等于当前元素的值,也就是题目的答案,即「下一个更大元素」

别看 for 里面套了 while 循环就以为是 O(n^2) 时间复杂度,实际上是 O(n),因为每个元素最多被 push 或 pop 1 次。

再看一道题目

503. 下一个更大元素 II

image.png

这题还是典型的单调栈题目,只不过加大了点难度,说是这个数组是循环的,其实仔细想下:只要把数组再复制一份,加到后面即可,一般循环相关题目,都可以先考虑复制一份的思路。

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)
};