单调栈-lc503

127 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情

单调栈就是针对求下一个更大、小的元素的题目的算法,可以参考leedcode503. 下一个更大元素 II 这道题

503. 下一个更大元素 II

image-20220809162940615.png 在[6,5,4,3,8]中,对于前面的 [6,5,4,3] 等数字都需要向后遍历,当寻找到元素 8 时才找到了比自己大的元素;而如果已知元素 6 向后找到元素 8 才找到了比自己的大的数字,那么对于元素 [5,4,3] 来说,它们都比元素 6 更小,所以比它们更大的元素一定是元素 8,不需要单独遍历对 [5,4,3] 向后遍历一次!

在单调栈中,遍历数组,栈中因为空先存入6(存的是下标0),而5,4,3因为比6小也依次存入【说明当前元素的「下一个更大元素」与栈顶元素相同】,遍历到8时,8比栈顶3大,那么元素3他的下一个更大元素就是8【说明当前元素是前面一些元素的「下一个更大元素」】,把3下标元素改为8.这就是核心。8继续和栈顶4比,依次类推。

因为8也要找比它大的,所以数组要循环两次。

暴力

每个元素往后找比自己大的数,数组多长就往后找几次,越界了可以

1 拼长度 nums = nums.concat(nums)

2 取余 nums[(i+item)%len]

var nextGreaterElements = function(nums) {
    let len = nums.length,res = []
    nums = nums.concat(nums)
    for(let i=0;i<len;i++){
        let time = 0,better = -1
        while(time<len-1){
            time++
            // 或者nums[(i+item)%len]
            if(nums[i+time]>nums[i]){
                better = nums[i+time]
                break
            }
        }
        res.push(better)
    }
    return res
};
时间复杂度 o(n^2)

单调栈

// 单调栈,遍历nums循环两次成环,栈内存的是下标,遍历到比栈顶大的数时,这个数就是比栈顶指向元素更大的一个数
    // 弹出栈顶,将栈顶指向的坐标元素 改为 当前数
	let stack = [],len = nums.length
    let res = new Array(len).fill(-1)
    for(let i=0;i<len*2;i++){
        let num = nums[i%len]
        // 持续和栈顶比较,大的话弹出
        while(stack.length&&num>nums[stack.at(-1)]){
            res[stack.pop()] = num
        }
        stack.push(i%len)
    }
    return res

宫水三叶的说法

对于「找最近一个比当前值大/小」的问题,都可以使用单调栈来解决。

单调栈就是在栈的基础上维护一个栈内元素单调。

在理解单调栈之前,我们先回想一下「朴素解法」是如何解决这个问题的。

对于每个数而言,我们需要遍历其右边的数,直到找到比自身大的数,这是一个 O(n^2)的做法。

之所以是 O(n^2),是因为每次找下一个最大值,我们是通过「主动」遍历来实现的。

而如果使用的是单调栈的话,可以做到 O(n)O(n) 的复杂度,我们将当前还没得到答案的下标暂存于栈内,从而实现「被动」更新答案。

也就是说,栈内存放的永远是还没更新答案的下标。

具体的做法是:

每次将当前遍历到的下标存入栈内,将当前下标存入栈内前,检查一下当前值是否能够作为栈内位置的答案(即成为栈内位置的「下一个更大的元素」),如果可以,则将栈内下标弹出。

如此一来,我们便实现了「被动」更新答案,同时由于我们的弹栈和出栈逻辑,决定了我们整个过程中栈内元素单调。

还有一些编码细节,由于我们要找每一个元素的下一个更大的值,因此我们需要对原数组遍历两次,对遍历下标进行取余转换。

以及因为栈内存放的是还没更新答案的下标,可能会有位置会一直留在栈内(最大值的位置),因此我们要在处理前预设答案为 -1。而从实现那些没有下一个更大元素(不出栈)的位置的答案是 -1。

作者:AC_OIer 链接:leetcode.cn/problems/ne… 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

496. 下一个更大元素 I

image-20220809162956224.png

var nextGreaterElement = function(nums1, nums2) {
    // 单调栈,数组2制表,用map存,比arr好
    // 参考:https://leetcode.cn/problems/next-greater-element-i/solution/xia-yi-ge-geng-da-yuan-su-i-by-leetcode-bfcoj/
    let map = new Map(),stack = []
    for(let i =0;i<nums2.length;i++){
        let num = nums2[i]
        // 这里不存索引,改存值
        while(stack.length&&stack.at(-1)<num){
            let p = stack.pop()
            // 存储弹出的栈顶 和 它的下一个最大数
            map.set(p,num)
        }
        stack.push(num)
    }
    // 没被弹出的就是找不到,返回-1
    return nums1.map(i=>map.get(i)||-1)
};

739. 每日温度

image-20220809163215216.png

var dailyTemperatures = function(temperatures) {
    // 存索引,弹栈时,要存的结果是 当前索引-栈顶索引
    let stack = [],len = temperatures.length,res = new Array(len).fill(0)
    for(let i=0;i<len;i++){
        let num = temperatures[i]
        while(stack.length&&temperatures[stack.at(-1)]<num){
            let p = stack.pop()
            res[p] = i-p
        }
        stack.push(i)
    }
    return res
};

84. 柱状图中最大的矩形

image-20220809174537664.png

/**
 * @param {number[]} heights
 * @return {number}
 */
var largestRectangleArea = function (heights) {
    // 1 以前可以,现在超时了
    // 原理是 遍历元素,以当前元素为最高,找左边和右边,算出面积,
  let area = 0;

  for (let i = 0; i < heights.length; i++) {
    const height = heights[i];
    let left = i;
    let right = i;

    // 向前遍历查找左边界:
    // 判定边界的条件是遇到下一个高度大于等于当前高度。则表明当前left为左边界
    // 如果说直接判断heights[left],会出现退出循环时,left多走了一步,造成计算宽度时很难处理,因此判断left - 1
    // 如果left为0,heights[-1]为undefined,heights[-1]>=height都为false
    while (heights[left - 1] >= height) {
      left--;
    }

    // 向前遍历查找右边界:
    // 判定边界的条件是遇到下一个高度大于等于当前高度。则表明当前right为右边界
    // 如果说直接判断heights[right],会出现退出循环时,right多走了一步,造成计算宽度时很难处理,因此判断right + 1
    // 如果right为heights.length,heights[heights.length]为undefined,heights[heights.length]>=height都为false
    while (heights[right + 1] >= height) {
      right++;
    }

    // 根据当前查找的边界计算宽度
    const width = right - left + 1;
    // 计算当前面积,并与已存储的面积对比,取最大值
    area = Math.max(area, width * height);
  }

  return area;
    // 2 、 单调栈,可以看成是上面的优化版
    // 参考 https://leetcode.cn/problems/largest-rectangle-in-histogram/solution/wo-yong-qiao-miao-de-bi-yu-jiang-dan-diao-zhan-jie/
    // 单调递减栈,找出下一个更小的元素,算是求出右边界,
    // 左边界则是栈顶前一个元素,因为栈是递增,
    // 如果矩形是 [1,2,3] 那永远不出栈,所以后面补个0
    // 前面补0,是为了左边界的计算,栈无元素时,拿不到左边界,所以补0代替
    heights = [0,...heights,0]
    let stack = [],len = heights.length,max = 0
    for(let i=0;i<len;i++){
        let height = heights[i]
        while(stack.length&&heights[stack.at(-1)]>height){
            let p = stack.pop()
            // let area = heights[p]*(i-p)
            let area = heights[p]*(i-stack.at(-1)-1)
            max = Math.max(max,area)
        }
        stack.push(i)
    }
    return max
};

其他题

leetcode.cn/tag/monoton…

铭记

求下一个更大、小的元素 单调栈!!

模板:

  for(let i=0;i<len;i++){
        let num = nums[i]
        // 持续和栈顶比较,大的话弹出
        while(stack.length&&num>nums[stack.at(-1)]){
            res[stack.pop()] = num
        }
        stack.push(i)
    }