84.柱状图中最大的矩形(单调栈)

87 阅读6分钟

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

 

示例 1:

输入: heights = [2,1,5,6,2,3]
输出: 10
解释: 最大的矩形为图中红色区域,面积为 10

示例 2:

输入: heights = [2,4]
输出: 4

 

提示:

  • 1 <= heights.length <=105
  • 0 <= heights[i] <= 104

方法一:

三次遍历:

方法二:

两次遍历:

为了做到两次遍历,以及写法三的一次遍历,首先,把 right[i] 的定义略作修改,调整为:在 i 右侧的小于或等于 h=heights[i] 的最近元素的下标。

如果 heights 中没有相同的元素,这样修改不影响 right[i]。

如果 heights 中有相同的元素呢?比如 heights=[1,3,4,3,2],左边那个 3 的 right[i] 会变小,导致矩形面积变小,这是否会导致计算错误?

不会。注意在这种情况下,这两个高为 3 的柱子,对应的矩形面积(在写法一中)是一样大的,虽然(在写法二中)左边那个 3 的矩形面积变小了,但右边那个 3 的矩形面积是不变的,所以我们不会错过正确答案。

修改 right[i] 的定义后,我们可以把 left 和 right 合在一起计算,从而减少一次遍历:

在计算 left 的过程中,如果栈顶元素 ≥heights[i],那么 i 就是栈顶元素的 right。

疑问解析:

1.在二次遍历中,为什么弹出的柱子right下标就是i 请详细解释一下


前提

  • 我们用一个 单调递增栈 存放柱子的下标。

  • 遍历当前柱子 heights[i] 时:

    while (!st.isEmpty() && heights[st.peek()] >= heights[i]) {
        right[st.pop()] = i;
    }
    
  • right[j] 的定义:第 j 个柱子右边第一个 小于或等于 heights[j] 的柱子下标。


为什么是 i?

假设栈顶柱子下标是 j,即 st.peek() = j

  1. 栈里保存的元素是 递增的柱子下标,也就是:

    heights[stack[0]] < heights[stack[1]] < ... < heights[j]
    

    或者至少不下降(>=)

  2. 当前柱子高度 heights[i] 比栈顶柱子矮:

    heights[i] <= heights[j]
    

    说明我们找到了 j 右边 第一个小于等于 heights[j] 的柱子

  3. 由此,right[j] = i 就成立:

    • 右边第一个比 heights[j] 小或等于的柱子就是当前柱子 i
    • 不可能是更右边的元素,因为我们还没有遍历到它们

图解理解

假设 heights = [2, 1, 5, 6, 2, 3],我们遍历到 i = 4(柱子高度 2):

栈状态(存下标):

栈元素heights
25
36
  • 当前柱子 heights[4] = 2
  • 栈顶 6 >= 2 → 弹出 3,设置 right[3] = 4
  • 弹出 2,设置 right[2] = 4

解释

  • 对柱子 6(下标 3)来说,右边第一个 ≤6 的柱子就是下标 4 的 2 → right[3]=4
  • 对柱子 5(下标 2)来说,右边第一个 ≤5 的柱子也是下标 4 的 2 → right[2]=4

为什么不用更右边的柱子?

  • 栈保证从左到右递增
  • 当前柱子 i 是 第一个比栈顶柱子矮或等的柱子
  • 栈中元素对应的柱子 左边没有被弹出,右边第一个比它矮的柱子就是 i
  • 如果我们不立即弹出,可能会错过最小右边界

总结

关键逻辑

  1. 栈顶柱子比当前柱子高(或相等) → 当前柱子就是右边第一个比它矮或等的柱子
  2. 弹出栈顶并设置 right[栈顶] = i
  3. 保持栈单调递增,继续处理栈中下一个柱子

所以在遍历过程中,每次弹出的柱子都能准确得到它的 右边界


2.为什么构建right数组要从右向左遍历

这个问题问得非常到位 👍,为什么 right 要从右往左遍历?
我们分几步来看。


📌 背景:right[i] 表示什么?

  • right[i] = 从位置 i 往右看,右边第一个比 heights[i] 小的柱子的位置
  • 如果右边没有更小的,就设为 n

📌 为什么要「从右往左」遍历?

1. 单调栈的构建逻辑

  • 我们要找到「右边第一个比自己小」→ 就是 Next Smaller Element 问题。
  • 如果我们从 左往右遍历,我们看到 i 时,右边的元素还没处理,不知道「下一个更小」在哪里。
  • 所以必须 从右往左遍历,这样当我们到 i 时,右边的所有情况(右边的柱子)已经在栈里了。

2. 栈存的是什么?

  • 在遍历过程中,栈始终是一个「单调递增栈」(存下标)。
  • 保证栈顶元素就是右边第一个比当前小的柱子。

例如:
heights = [2, 1, 5, 6, 2, 3]

从右往左:

  • i=5 (h=3):右边没有,right[5]=n=6,栈=[5]
  • i=4 (h=2):右边第一个比 2 小的不存在 → right[4]=6,栈=[4,5]
  • i=3 (h=6):栈顶=5(h=3)<6 → right[3]=5
  • i=2 (h=5):栈顶=3(h=6)≥5 弹出 → 栈顶=5(h=3)<5 → right[2]=5

3. 如果从左往右会怎样?

假设 heights = [2, 1, 5, 6, 2, 3],我们从左往右:

  • i=0, h=2:右边的 1、5、6 都还没看 → 根本不知道答案。
  • 只能等处理到 i=1 时,才知道 i=0 的右边有个 1 更小 → 这就需要 回头更新,逻辑会复杂很多。

单调栈就是为了避免这种“回头处理”,所以我们反向遍历,边走边确定答案。


📌 总结

  • right[i] 要找 右边第一个比自己小的柱子
  • 为了做到「一次遍历直接确定答案」,必须 从右往左遍历
  • 这样栈里始终保存的是「i 右边的递增柱子下标」,栈顶自然就是答案。

方法三:一重遍历 写法二告诉我们,栈顶出栈时,当前下标就是栈顶的 right。

如果此刻能顺带求出栈顶的 left,那不就能一步到位,一次遍历就搞定了?

想一想,栈顶的 left 在哪?

由于单调栈是底小顶大的,栈顶下面那个柱子的高度一定比栈顶小,所以栈顶下面的值就是 left。

为简化代码逻辑,可以在一开始把 −1 入栈,当作哨兵。当栈中只有一个数的时候,栈顶下面那个数刚好就是 −1,对应 left[i]=−1 的情况。

此外,循环结束的时候,栈中还有数据,这些数据也要计算矩形面积。处理这种情况可以再写一个循环,但更简单的办法是,往 heights 的末尾加一个 −1(或者任意 ≤min(heights) 的数),从而保证循环结束的时候,栈一定是空的(不包括哨兵)。

Python3 Java Java 数组 C++ C Go JavaScript Rust