前端算法入门之路(十四)(单调栈及经典问题)

180 阅读2分钟

单调栈

单调栈是一个递增或递减的栈结构,可以用来维护最近的【大于/小于】关系
入栈操作:栈顶入栈,依次把破坏单调性的元素出栈(维护单调性)
从左侧入栈就是维护左侧的最近关系
从右侧入栈就是维护右侧的最近关系

LeetCode肝题

    1. 最小栈
// 设置一个栈用来存储最小值,当栈为空或者新push的值小于等于栈顶元素时,往栈里添加这个元素
// 出栈时如果arr出栈元素等于stack栈顶元素的话stack出栈
var MinStack = function() {
    this.arr = []
    this.stack = []
};
MinStack.prototype.push = function(val) {
    this.arr.push(val)
    if (this.stack.length == 0 || this.stack[this.stack.length - 1] >= val) {
        this.stack.push(val)
    }
};
MinStack.prototype.pop = function() {
    let v = this.arr.pop()
    if (v == this.stack[this.stack.length - 1]) this.stack.pop()
};
MinStack.prototype.top = function() {
    return this.arr[this.arr.length - 1]
};
MinStack.prototype.getMin = function() {
    return this.stack[this.stack.length - 1]
};
    1. 下一个更大元素 II
// 查询元素右侧的最近大于关系,要用单调递减栈
// 如果新入栈元素大于栈顶元素了,说明栈顶元素的下一个更大元素就是新入栈元素
// 循环数组搜索的话直接搜两遍即可
var nextGreaterElements = function(nums) {
    let stack = [], ans = new Array(nums.length).fill(-1)
    for(let i = 0; i < nums.length; i++) {
        while(stack.length && nums[i] > nums[stack[stack.length - 1]]) {
            ans[stack.pop()] = nums[i]
        }
        stack.push(i)
    }
    for(let i = 0; i < nums.length; i++) {
        while(stack.length && nums[i] > nums[stack[stack.length - 1]]) {
            ans[stack.pop()] = nums[i]
        }
        stack.push(i)
    }
    return ans
};
    1. 股票价格跨度
// 也是查找最近的大于关系,利用一个字段t来计算跨度
// 可以放一个最大值元素在栈里保证不会空栈
// t的差值就是最终要返回的跨度
var StockSpanner = function() {
    this.t = 0
    this.s = [{p: Infinity, t: this.t++}]
};
StockSpanner.prototype.next = function(price) {
    while(price >= this.s[this.s.length - 1].p) this.s.pop()
    let ans = this.t - this.s[this.s.length - 1].t
    this.s.push({p: price, t: this.t++})
    return ans
};
    1. 每日温度
// 同样的最近大于关系,使用单调递减栈
// ans存储下标差值
var dailyTemperatures = function(tem) {
    let s = [], ans = new Array(tem.length).fill(0)
    for (let i = 0; i < tem.length; i++) {
        while(s.length && tem[i] > tem[s[s.length - 1]]) {
            let ind = s.pop()
            ans[ind] = i - ind
        }
        s.push(i)
    }
    return ans
};
    1. 柱状图中最大的矩形
// 本质上是求当前柱子两边的最近小于关系,算出两侧坐标相减再减一即可得到矩形的宽
// 设置两个数组分别存储左侧和右侧最近小于关系,最后计算面积取最大值
var largestRectangleArea = function(heights) {
    let n = heights.length, ans = 0, l = new Array(n).fill(-1), r = new Array(n).fill(n), s = []
    for(let i = 0; i < n; i++) {
        while(s.length && heights[i] < heights[s[s.length - 1]]) {
            let ind = s.pop()
            r[ind] = i
        }
        if (s.length) l[i] = s[s.length - 1]
        s.push(i)
    }
    for(let i = 0; i < n; i++) {
        ans = Math.max(ans, (r[i] - l[i] - 1) * heights[i])
    }
    return ans
};
    1. 子数组最小乘积的最大值
首先假设当前元素为最小值,那么它所对应的子数组为从它到两侧的第一个小于它的位置
所以求最近的小于关系,要用到单调递增栈
设置左右两个数组分别记录当前元素两侧最近的小于它的位置
求区间和要用前缀和数组
最后遍历所有数字,取乘积最大的就是答案
本题数字比较大所以需要BigInt,否则数字有误差,离正确答案差了一点
var getResult = function(root, k, ans) {
    if (!root) return
    if (k ==ans.length) ans.push([])
    ans[k].push(root.val)
    getResult(root.left, k+1, ans)
    getResult(root.right, k+1, ans)
}
var resves = function(arr) {
    for(let i=0,j=arr.length-1;i<j;i++,j--) {
        [arr[i], arr[j]] = [arr[j], arr[i]]
    }
}
var zigzagLevelOrder = function(root) {
    if (!root) return []
    let ans = []
    getResult(root, 0, ans)
    for(let i = 0; i<ans.length; i++) {
        if (i%2==1) {
            resves(ans[i])
        }
    }
    return ans
};
    1. 子数组的最小值之和
// 定义一个sum存储以当前位置为最小值的RMQ问题答案之和
var sumSubarrayMins = function(arr) {
    let s = [], sum = new Array(arr.length + 1).fill(0), ans = 0 
    for (let i = 0; i < arr.length; i++) {
        while(s.length && arr[i] <= arr[s[s.length - 1]]) s.pop()
        let ind = s.length ? s[s.length - 1] : -1
        s.push(i)
        sum[s.length] = sum[s.length - 1] + arr[i] * (i - ind)
        ans = ans + sum[s.length]
    }
    return ans % 1000000007
};
    1. 下一个更大元素 I
// 将nums2数组里的下一个更大元素对照关系存入obj中,遍历num1依次存储
var nextGreaterElement = function(nums1, nums2) {
    let obj = {}, s = [], ans = []
    for(let i = 0; i < nums2.length; i++) {
        while(s.length && nums2[i] > [s[s.length - 1]]) {
            obj[s.pop()] = nums2[i]
        }
        s.push(nums2[i])
    }
    while(s.length) obj[s.pop()] = -1
    for(let i = 0; i < nums1.length; i++) {
        ans.push(obj[nums1[i]])
    }
    return ans
};
    1. 132 模式
// 先使用l数组记录元素左侧最小的值
// 倒序遍历nums,使用单调递减栈,将弹出栈的元素记录到val中,最后一个弹栈的元素就是元素右侧小于它的较大值
// 如果符合条件则返回true,最后返回false
// 该判定不能判断有多少对,只能判定存不存在
var find132pattern = function(nums) {
    let s = [], l = Array(nums.length)
    l[0] = Infinity
    for(let i = 1; i < nums.length; i++) l[i] = Math.min(l[i - 1], nums[i - 1])
    for(let i = nums.length - 1; i >= 0; i--) {
        let val = nums[i]
        while(s.length && nums[i] > s[s.length - 1]) {
            val = s.pop()
        }
        s.push(nums[i])
        if (val > l[i] && l[i] < nums[i] && val < nums[i]) return true
    }
    return false
};
    1. 接雨水
// 使用单调递减栈可以得到V型结构,也就是蓄水的位置
// 元素弹栈的过程中,如果栈内还存在元素,说明左侧还有更高的柱子
// 弹出元素弹走的柱子所承载的水量=当前元素坐标-栈顶元素-1 * 当前元素与弹出元素高度差和栈顶元素与弹出元素的高度差的较小值
// 将所有乘水量加起来就是最终答案
var trap = function(height) {
    let s = [], ans = 0
    for(let i = 0; i < height.length; i++) {
        while(s.length && height[i] > height[s[s.length - 1]]) {
            let ind = s.pop()
            if (s.length == 0) continue
            ans += (i - s[s.length - 1] - 1) * Math.min(height[i] - height[ind], height[s[s.length - 1]] - height[ind])
        }
        s.push(i)
    }
    return ans
};