给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
示例 1:
输入: heights = [2,1,5,6,2,3]
输出: 10
解释: 最大的矩形为图中红色区域,面积为 10
示例 2:
输入: heights = [2,4]
输出: 4
提示:
1 <= heights.length <=1050 <= 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:
-
栈里保存的元素是 递增的柱子下标,也就是:
heights[stack[0]] < heights[stack[1]] < ... < heights[j]或者至少不下降(>=)
-
当前柱子高度
heights[i]比栈顶柱子矮:heights[i] <= heights[j]说明我们找到了
j右边 第一个小于等于 heights[j] 的柱子。 -
由此,
right[j] = i就成立:- 右边第一个比 heights[j] 小或等于的柱子就是当前柱子 i
- 不可能是更右边的元素,因为我们还没有遍历到它们
图解理解
假设 heights = [2, 1, 5, 6, 2, 3],我们遍历到 i = 4(柱子高度 2):
栈状态(存下标):
| 栈元素 | heights |
|---|---|
| 2 | 5 |
| 3 | 6 |
- 当前柱子
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
- 如果我们不立即弹出,可能会错过最小右边界
总结
关键逻辑:
- 栈顶柱子比当前柱子高(或相等) → 当前柱子就是右边第一个比它矮或等的柱子
- 弹出栈顶并设置
right[栈顶] = i - 保持栈单调递增,继续处理栈中下一个柱子
所以在遍历过程中,每次弹出的柱子都能准确得到它的 右边界。
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