[Golang修仙之路] 算法专题:单调栈

75 阅读4分钟

image.png

写在前面:这个博客完全是学习了灵神之后,为了加深自己印象而写的,代码和思路并非原创。零零星星自己的理解和草图是自己的而已,90%的内容来自灵神的视频和题单。

单调栈用来解决什么问题?

解决:”一个数组,找元素x右边第一个比x大的数“ 这类问题。

1. 每日温度

单调栈模板题:739. 每日温度

思考过程:

单调栈笔记.jpg

代码:

func dailyTemperatures(temperatures []int) []int {
    n := len(temperatures)
    ans := make([]int, n) // n个元素,初始值为0
    st := []int{}
    for i := n - 1; i >= 0; i-- {
        t := temperatures[i]
        // 大于等于,而不是大于的原因是:
        // 在相同的情况下,更靠近左边的元素应该被留下
        // 而从右往左遍历,自然越后出现的越靠左边啦
        for len(st) > 0 && t >= temperatures[st[len(st)-1]] {
            st = st[:len(st)-1]
        }
        if len(st) > 0 {
            ans[i] = st[len(st)-1] - i
        }
        st = append(st, i)
    }
    return ans
}

代码时间复杂度:O(N)

因为每个元素都只入栈一次。

理解深了每日温度后,可以举一反三下列题目:

2. 柱状图中的最大矩形

这是可以用单调栈解决的第二类问题,本质上还是求「右边第一个比x小和左边第一个比x小的数」。

2.1 题目描述 && 思路

问题描述:

image.png

思路:

矩形面积 = 长 * 宽

长肯定是每个柱子的高度中的一个,可以用反证法确定这一点。那么我们就可以枚举每个高度作为矩形的「长」。

现在就1个问题,矩形的宽怎么确定?

直接说答案:高度的左边右边第一个比它小的作为边界,中间的部分就是宽。

这样做你冥冥之中就感觉是对的,因为比x小的那个高度y,在枚举y的时候,就已经考虑过了。

行了,现在的问题变成如何求「左边右边第一个比它小的」,这不就是单调栈的拿手好戏了吗!

代码:

2.2 心得:这种题目的代码怎么想?

目标是找x右边「第一个」比x 「小」的,那么符合哪两个条件的应该放进来?

  • 更靠左边。

所以来了一个元素,如果比栈顶的小或者相等,栈顶的元素应该滚蛋。

2.3 代码

func largestRectangleArea(heights []int) int {
    // 找右边第一个比自己小的
    // 栈应该从下往上越来越大
    st := []int{}
    n := len(heights)
    right, left := make([]int, n), make([]int, n)
    for i := n - 1; i >= 0; i-- {
        h := heights[i]
        for len(st) > 0 && h <= heights[st[len(st)-1]] {
            st = st[:len(st)-1]
        }
        // 初始为n
        right[i] = n
        if len(st) > 0 {
            right[i] = st[len(st)-1]
        }
        st = append(st, i)
    }
    st = []int{}
    for i := 0; i < n; i++ {
        h := heights[i]
        for len(st) > 0 && h <= heights[st[len(st)-1]] {
            st = st[:len(st)-1]
        } 
        // 初始为-1
        left[i] = -1
        if len(st) > 0 {
            left[i] = st[len(st)-1]
        }
        st = append(st, i)
    }
    // fmt.Println(left, right)
    ans := 0
    for i, h := range heights {
        ans = max(ans, h * (right[i] - left[i] - 1))
    }
    return ans
}

2.4 举一反三

会了这个题,那你可NB了,可以直接做这三个题。

1793题:无非是在最后求面积的时候 增加一些if判断,注意你的left和right数组的值,跟题目描述的不完全一样。

85题: 最大矩形给了一个矩阵,别怕,对每一行做一个「柱状图中的最大矩形」。

221题:正方形?别慌,矩形的两条边找出来更短的那一条,然后平方即可。

3. 接雨水

这个题有双指针的解法,我觉得我就单调栈了。

思路:

I2R1A775n4w5Y38310Z9r5_vHw.jpg

当一个元素x来了,如果他比栈顶大,栈顶做为bottomH,弹出。

新的栈顶y。min(height[x], height[y]) - bottomH 作为水的高。

x - y - 1 作为水的宽。

代码:

func trap(height []int) int {
    ans := 0
    st := []int{}
    for i, h := range height {
        for len(st) > 0 && h >= height[st[len(st)-1]] {
            bottomH := height[st[len(st)-1]]
            st = st[:len(st)-1]
            if len(st) == 0 {
                break
            }
            left := st[len(st)-1]
            dh := min(h, height[left]) - bottomH
            ans += dh * (i - left - 1)
        }
        st = append(st, i)
    }
    return ans
}