单调栈策略模板归纳

单调栈模板总结

基本概念不再多说了。因为个人常常在细节上出错,所以总结一下单调栈的考虑点和写法。

ordered_stack.png

如上图求解最大面积,由遍历的思想,遍历每一个位置,以此位置为高的最大宽度应该是 左边界为向左小于点的位置后右边界为向右小于此点的位置前 之间。即上图 B 作用的宽度为 (A, C) 之间(前开后开)。

A、C 分别为 B 作用范围取不到的左边界和右边界,为了方便下面的表述,下文就用 左边界 和 右边界 来表述。说法不严谨,但知道意思即可。

单调递增栈的解法流程

由上分析,所以我们需要找的是每一个点它的左右小于此值的位置。所以:

  1. 首先要明白一个前提,单调递增栈关键要存储的是 位置,通过原数组能拿到值;
  2. 单调递增栈因为递增,所以栈中每一个元素的左边界都是栈中的前一个元素;
  3. 上条中栈底位置的左边界要特殊处理
  4. 每一个栈外的元素可通过与栈顶元素进行比较,判断是否为栈顶元素的右边界(栈顶元素可以先弹出比较,也可以不弹出比较)
  5. 如果栈外元素 > 栈顶元素,此栈外元素入栈,进而维护了 单调递增栈
  6. 如果栈外元素 < 栈顶元素,即找到了栈顶元素的 右边界,即可以按左右边界计算作用面积
  7. 上步中弹出栈顶元素后,将栈外元素继续与栈顶元素比较,即跳转到 4,直到满足 5 中的条件,分析下一个栈外元素
  8. 直到所有栈外元素分析完,最后一个元素最终会入栈,最终只剩下栈中的元素还没有计算作用面积
  9. 依次弹出栈中元素,右边界为数组溢出位置的第一个下标,左边界为栈中前一个元素,计算作用面积
  10. 直到栈为空,即计算了所有位置的作用面积

由上流程,可以总结一下:

  • 问题的关键在于如何找到一个元素它的左右边界
  • 利用单调递增栈可以很容易找到某元素的左边界
  • 通过栈外元素与栈顶的比较,能找到栈顶元素的右边界
  • 注意上两条,可以得出,单调栈相关算法每次分析的元素是 栈顶元素

写代码注意问题

  • 因为每次判断的是栈顶元素,所以栈顶要不要出栈,哪种方式编程最简单?
    • 出栈后若栈非空则此时的栈顶元素为左边界
    • 出栈后与栈外元素比较,若栈外元素大,需要将两个都放入栈中
    • 出栈后若栈外元素小,则应该循环出栈,直到此栈外元素大于栈顶或栈空,入栈
  • 栈底元素的左边界问题,数组最后一个元素的右边界问题
    • 常见的逻辑会在循环完最后一个元素后,单独循环栈中的元素出栈,即两个主循环操作(其实第一个内部还有一个循环,总共三个)
    • 如果在原数组最左边放一个最小值,最右边放一个最小值将不用再考虑第二个循环(但注意可行性,注意不要取溢出位置的数组元素)
  • 判断栈顶与栈外元素的大小用大于还是大于等于(最后是 else)?
    • 这个要看情况,如果数组中没重复元素则没问题
    • 如果数组中有重复元素,但结果只求最大面积,那也没问题
    • 如果数组中有重复元素,但要求每个位置对应的作用面积,则需要额外添加存储结构(思考下左边界和右边界就明白了)

模板

以 剑指 Offer II 039. 直方图最大矩形面积 为例,讲解规划此算法的策略。让其只用一个主循环逻辑(嵌套一个算两个了)。

主要策略:

  • 栈底放一个 虚拟的边界 来解决所有节点的左边界的问题(虚拟边界 的取值靠封装取值函数保证取值安全,取值返回一个取不到的小值)
  • 数组最外层也放一个 虚拟的边界 以保证栈中元素可以都弹出来
  • 封装数组取值,覆盖虚拟边界取到比列表中最小值还小的值

第二条是去除单独的循环考虑弹出栈内元素的关键。

下方,虚拟节点 heights[-1]height[len(height)-1] 都设置为 -1,这样在代码 ① 位置注意不能取到 =,否则可能不存在 peek,如果将 heights[-1] 设为 -2 的话,① 处的条件可以取到 =


func largestRectangleArea(heights []int) int {
    stack := make([]int, 0, len(heights)) // 初始化栈

    // 初始放入 虚拟边界 和 第一个下标(虚拟边界肯定小于任何下标的值)
    stack = append(stack, -1, 0)

    // 用安全的取值函数封装取数组的取值
    getVal := func(i int ) int {
        if i < 0 || i >= len(heights) {
            return -1
        }
        return heights[i]
    }

    max := 0
    for i := 1; i <= len(heights); i++ {
        for { // 栈底有个占位的最小值,所以肯定有一个 pop 的值
            var pop int
            // 先 pop 出栈顶比较好,因为 peek 也可以通过栈顶元素获取
            pop, stack = stack[len(stack)-1], stack[:len(stack)-1]

            if getVal(pop) > getVal(i) { // ①
                peak := stack[len(stack) - 1] // 注意 peek 要在条件内,因为此条件外不保证栈中一定有元素
                if width := i - peak - 1; getVal(pop)*width > max {
                    max = getVal(pop) * width
                }
            } else if  getVal(pop) < getVal(i){
                stack = append(stack, pop)
                break
            } else { // equal
                break // 因为引入了占位的值,所以需要考虑 equal 的情况
                // 此种情况需要按题进行判断
            }
        }

        stack = append(stack, i) // 放入元素
    }

    return max
}

练习题

  • 剑指 Offer II 039. 直方图最大矩形面积
  • 剑指 Offer II 040. 矩阵中最大的矩形

你可能感兴趣的:(单调栈策略模板归纳)