单调栈

常用算法与数据结构模板系列(八)

Posted by John Mactavish on October 6, 2020

单调栈也是栈,特别之处在于栈内的元素都保持有序(单调递增或单调递减)。 它通常只处理一类典型的问题,叫做 Next Greater Element:“给你一个数组,返回一个等长的数组,对应索引存储着向右找的下一个更大元素,如果没有更大的元素,就存 -1。”当然,“向右找的下一个更小元素”、 “向左找的下一个更大元素”等也是这一类的。

1: 当前项向右找第一个比自己大的位置 —— 从右向左维护一个单调递减栈

def nextGreaterElement_01(nums: list):
    length = len(nums)
    res, stack = [-1] * length, []

    for i in range(length - 1, -1, -1):
        # 想一想比较时用 “<=” 与 “<” 有什么区别吗?
        while stack and stack[-1] <= nums[i]:
            stack.pop()
        if stack:
            res[i] = stack[-1]
        # 如果栈为空,则为默认值 -1,代表不存在这样的数
        stack.append(nums[i])

    return res

或者 —— 从左向右维护一个单调递减栈

def nextGreaterElement_011(nums: list):
    length = len(nums)
    res, stack = [-1] * length, []

    for i in range(length):
        while stack and nums[stack[-1]] < nums[i]:
            idx = stack.pop()
            res[idx] = nums[i]
        stack.append(i)

    return res

不难发现,这样操作更难想到,因此知道就好,一般用方法一即可。

2:当前项向右找第一个比自己小的位置 —— 从右向左维护一个单调递增栈

def nextGreaterElement_02(nums: list):
    length = len(nums)
    res, stack = [-1] * length, []

    for i in range(length - 1, -1, -1):
        while stack and nums[stack[-1]] >= nums[i]:
            stack.pop()
        if stack:
            res[i] = stack[-1]
        stack.append(nums[i])

    return res

3: 当前项向左找第一个比自己大的位置 —— 从左向右维护一个单调递减栈

def nextGreaterElement_03(nums: list):
    length = len(nums)
    res, stack = [-1] * length, []

    for i in range(length):
        while stack and nums[stack[-1]] <= nums[i]:
            stack.pop()
        if stack:
            res[i] = stack[-1]
        stack.append(nums[i])

    return res

4: 当前项向左找第一个比自己小的位置 —— 从左向右维护一个单调递增栈

def nextGreaterElement_04(nums: list):
    length = len(nums)
    res, stack = [-1] * length, []

    for i in range(length):
        while stack and stack[-1] >= nums[i]:
            stack.pop()
        if stack:
            res[i] = stack[-1]
        stack.append(nums[i])

    return res

总结一下:

  • 当前项向右找第一个比自己大的位置 —— 从右向左维护一个单调递减栈
  • 当前项向右找第一个比自己小的位置 —— 从右向左维护一个单调递增栈
  • 当前项向左找第一个比自己大的位置 —— 从左向右维护一个单调递减栈
  • 当前项向左找第一个比自己小的位置 —— 从左向右维护一个单调递增栈

即:“当前项向右(左)找”就“从右向左(从左向右)维护栈”,“找第一个比自己小(大)的” 就用“单调递增(减)栈”。

有很多问题还可以转化为 Next Greater Element 来处理。比如 42. 接雨水

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:
输入:height = [4,2,0,3,2,5]
输出:9
 
提示:
n == height.length
0 <= n <= 3 * 104
0 <= height[i] <= 105

不难发现,积水的凹处都是两边高中间低的地方;那么对于一个元素,只要向右找到第一个 比它高的元素即可开始结算积水面积。然而这样会出现重复结算的问题,可以用哈希表避免, 但是更优的方法是把结算逻辑放在单调栈弹出元素时:

class Solution:
    def trap(self, height: List[int]) -> int:
        ans = 0
        stack = []
        for i in range(len(height) - 1, -1, -1):
            # 从右向左维护一个单调递减栈
            while stack and height[stack[-1]] < height[i]:
                h0 = height[stack.pop()]
                if stack:
                    h1 = min(height[i], height[stack[-1]])
                    ans += (h1 - h0) * (stack[-1] - i - 1)
            stack.append(i)
        return ans

这里其实都是找离当前项最近的元素,还有一类问题我将其称为 Furthest Greater Number, 它是找离当前项最远的更大的元素。例如:

962. 最大宽度坡

给定一个整数数组 A,坡是元组 (i, j),其中  i < j 且 A[i] <= A[j]。这样的坡的宽度为 j - i。

找出 A 中的坡的最大宽度,如果不存在,返回 0 。

示例 1:
输入:[6,0,8,2,1,5]
输出:4
解释:
最大宽度的坡为 (i, j) = (1, 5): A[1] = 0 且 A[5] = 5.

示例 2:
输入:[9,8,1,0,1,9,4,0,4,1]
输出:7
解释:
最大宽度的坡为 (i, j) = (2, 9): A[2] = 1 且 A[9] = 1.
 
提示:
2 <= A.length <= 50000
0 <= A[i] <= 50000

这同样是利用单调栈来解决的,不过用法有所不同:

class Solution:
    def maxWidthRamp(self, A: List[int]) -> int:
        stack = []
        for j, num in reversed(list(enumerate(A))):
            if not stack or num >= A[stack[-1]]:
                stack.append(j)
        ans = 0
        for i, num in enumerate(A):
            while stack and A[stack[-1]] >= num:
                ans = max(ans, stack.pop() - i)
        return ans

参考:wu-xian-sen-2 的 LeetCode 解答