在算法面试中, “柱状图中最大的矩形” (LeetCode 84)是一道极具代表性的经典题目。很多同学在初次遇到这道题时,往往会被各种边界条件绕晕。今天,我们将用最通俗的语言,带你彻底吃透这道题,掌握单调栈这一核心利器。
一、 题目解析:我们在找什么?
给定一个非负整数数组 heights,代表柱状图中各个柱子的高度。每个柱子的宽度为 1,我们需要找出其中可以勾勒出来的矩形的最大面积。
核心破题点:
不要一上来就想着“如何框选一个矩形”,而是要转换思维:以每一个柱子为基准,向左右两侧扩展,直到遇到比它矮的柱子。
也就是说,对于第 i 个柱子,我们需要找到:
- 左边第一个高度小于它的柱子索引
left。 - 右边第一个高度小于它的柱子索引
right。
那么,以第 i 个柱子为高的最大矩形面积就是:heights[i] * (right - left - 1)。遍历所有柱子,取最大值即可。
二、 从暴力到最优解的演进
1. 暴力枚举法(O(n²))
最直观的想法是:遍历每个柱子,然后分别向左右两边线性扫描,寻找第一个比它矮的柱子。
- 缺点:时间复杂度高达 O(n²),当数据量达到 10^5 时,必然超时。
2. 单调栈优化(O(n))
暴力法的核心瓶颈在于“向左右寻找第一个比它矮的柱子”这个过程太慢了。有没有办法在一次遍历中,就把所有柱子的左右边界都确定下来?
单调栈就是为此而生的。我们维护一个单调递增栈(栈底到栈顶的高度依次递增)。
核心逻辑:
当我们遍历到一个新柱子 i 时,如果它的高度小于栈顶柱子的高度,这就意味着:
- 对于栈顶柱子来说,当前柱子
i就是它右边第一个比它矮的柱子(右边界确定) 。 - 而栈顶下面的那个元素,就是它左边第一个比它矮的柱子(左边界确定) 。
此时,我们就可以弹出栈顶,并计算它的最大矩形面积!
三、 代码实现与深度解析
为了处理边界情况(比如数组是单调递增的,导致栈内元素无法全部弹出计算),我们通常会在数组首尾各加一个高度为 0 的哨兵。
/**
* @param {number[]} heights
* @return {number}
*/
var largestRectangleArea = function(heights) {
// 1. 哨兵技巧:首尾加0,确保所有元素都能入栈并出栈计算
heights = [0, ...heights, 0];
const stack = [];
let maxArea = 0;
for (let i = 0; i < heights.length; i++) {
// 2. 核心逻辑:当前高度 < 栈顶高度,说明找到了栈顶元素的右边界
while (stack.length && heights[i] < heights[stack[stack.length - 1]]) {
// 弹出栈顶,计算以该柱子为高的矩形面积
const heightIndex = stack.pop();
const height = heights[heightIndex];
// 3. 确定宽度:右边界是当前i,左边界是新的栈顶
// 如果栈空(理论上不会,因为有首哨兵0),左边界视为-1
const leftIndex = stack.length ? stack[stack.length - 1] : -1;
const width = i - leftIndex - 1;
maxArea = Math.max(maxArea, height * width);
}
// 当前索引入栈,维持单调递增
stack.push(i);
}
return maxArea;
};
代码深度解析(为什么这么写?)
-
为什么要加哨兵
[0, ...heights, 0]?- 首部的
0:保证栈永远不为空。当弹出最后一个有效元素时,新的栈顶就是这个0,避免了leftIndex的越界判断。 - 尾部的
0:这是最关键的。如果原数组是[1, 2, 3],遍历结束时栈里还剩下[1, 2, 3],它们都没有触发“遇到更小元素”的条件。尾部的0会强制触发while循环,把栈里剩下的元素全部弹出并计算面积。
- 首部的
-
栈里存的是索引还是值?
- 存的是索引
i。因为我们需要通过索引去获取高度heights[i],同时还需要用索引来计算宽度i - leftIndex - 1。
- 存的是索引
-
时间复杂度真的是 O(n) 吗?
- 是的。虽然代码里有一个
while嵌套在for循环中,但每个元素最多只会入栈一次、出栈一次。总的操作次数是 2n,所以均摊时间复杂度是 O(n)。
- 是的。虽然代码里有一个
四、 面试加分项
如果在面试中遇到这道题,除了写出上述代码,你还可以主动补充以下两点,展示你的工程思维:
- 空间换时间:单调栈本质上是用 O(n) 的空间复杂度,换取了 O(n) 的时间复杂度,这是算法设计中非常经典的权衡。
- 实际应用场景:这种“寻找左右两侧第一个更小/更大元素”的模型,在实际工程中常用于图像处理中的最大矩形检测、仓库货物摆放优化以及数据可视化中的柱状图分析等场景