1. 柱状图最大矩形问题概述
给定n个非负整数表示的柱状图,每个柱子的宽度为1,我们需要找出能够勾勒出的最大矩形面积。这个问题在LeetCode上编号为84题,是算法面试中的高频考点,也是单调栈应用的经典案例。
实际场景中,这个问题可以类比为城市规划中的地块利用率计算:假设每个柱子代表一栋建筑的高度,我们需要找出这片区域中能够建造的最大矩形广告牌的面积。这个广告牌必须完全包含在建筑轮廓内,不能超出任何建筑的高度限制。
2. 暴力解法与优化思路
2.1 基础暴力解法
最直观的解法是枚举所有可能的矩形:
python复制def largestRectangleArea(heights):
max_area = 0
n = len(heights)
for i in range(n):
min_height = float('inf')
for j in range(i, n):
min_height = min(min_height, heights[j])
max_area = max(max_area, min_height * (j - i + 1))
return max_area
时间复杂度O(n²),在LeetCode上会超时。这种方法的问题在于重复计算:对于每个起点i,都要重新计算从i开始的所有可能矩形。
2.2 中心扩展法优化
稍微优化的暴力解法是固定高度,向左右扩展:
python复制def largestRectangleArea(heights):
max_area = 0
n = len(heights)
for i in range(n):
left = i
while left >= 0 and heights[left] >= heights[i]:
left -= 1
right = i
while right < n and heights[right] >= heights[i]:
right += 1
max_area = max(max_area, heights[i] * (right - left - 1))
return max_area
虽然还是O(n²),但实际运行会比完全暴力稍快。这种方法的核心思想是:以每个柱子为矩形高度,找到左右第一个比它矮的柱子作为边界。
3. 单调栈解法详解
3.1 单调栈工作原理
单调栈是一种特殊的栈结构,它维护栈内元素的单调性(递增或递减)。对于本题,我们使用递增栈(从栈底到栈顶高度递增)。
关键观察点:
- 当遇到比栈顶矮的柱子时,说明找到了栈顶柱子右边界
- 栈中前一个元素就是左边界
- 计算当前矩形面积:高度×(右边界-左边界-1)
3.2 完整实现代码
python复制def largestRectangleArea(heights):
stack = []
max_area = 0
heights = [0] + heights + [0] # 添加哨兵
for i in range(len(heights)):
while stack and heights[stack[-1]] > heights[i]:
h = heights[stack.pop()]
w = i - stack[-1] - 1
max_area = max(max_area, h * w)
stack.append(i)
return max_area
3.3 算法执行流程示例
以heights = [2,1,5,6,2,3]为例:
- 初始化:stack = [], 添加哨兵[0,2,1,5,6,2,3,0]
- i=0: stack=[0]
- i=1: 2>0 → stack=[0,1]
- i=2: 1>0 → 弹出1,计算h=2,w=2-0-1=1 → area=2
- i=3: stack=[0,2]
- i=4: 5>1 → stack=[0,2,3]
- i=5: 6>5 → stack=[0,2,3,4]
- i=6: 2<6 → 弹出4,h=6,w=6-3-1=2 → area=12
- 继续比较2<5 → 弹出3,h=5,w=6-2-1=3 → area=15
- 最终max_area=15
4. 边界条件与特殊案例
4.1 常见边界情况
- 空输入:应返回0
- 单柱子:面积为柱子高度
- 完全递增序列:最大面积为最后一个柱子高度×1或第一个柱子高度×n
- 完全递减序列:需要正确处理左边界
4.2 哨兵技巧
在原始数组前后添加0作为哨兵:
- 开头的0保证栈永远非空,避免空栈判断
- 结尾的0保证所有柱子都会被计算
5. 复杂度分析与优化证明
时间复杂度:O(n),每个元素最多入栈出栈一次
空间复杂度:O(n),最坏情况下所有元素入栈
正确性证明:
- 每当遇到一个比栈顶小的元素时,栈顶元素右边界确定
- 栈中前一个元素就是左边界
- 通过维护栈的单调性,确保中间所有高于当前柱子的柱子都被正确处理
6. 实际面试中的变种问题
6.1 二维矩阵中的最大矩形
这是84题的扩展,LeetCode 85题。可以通过将二维问题转化为多个柱状图问题来解决:
- 对每一行计算柱状图高度
- 对每个柱状图调用本题解法
6.2 最大正方形
类似问题但限制为正方形,解法有所不同,通常使用动态规划。
6.3 带权重的柱状图
如果每个柱子有额外权重属性,需要调整面积计算公式。
7. 刷题建议与技巧
- 先理解暴力解法,再思考优化
- 画图辅助理解单调栈的工作过程
- 使用小规模测试案例手动模拟算法流程
- 记住模板但更要理解原理
- 同类问题集中练习(单调栈相关题目)
关键提示:面试时即使不记得单调栈解法,也应该先给出暴力解法并分析其不足,展示解题思路比直接给出最优解更重要。
8. 单调栈的其他应用场景
- 接雨水问题(LeetCode 42)
- 每日温度(LeetCode 739)
- 滑动窗口最大值(LeetCode 239)
- 下一个更大元素系列问题
理解这个问题的解法可以帮助解决一大类需要寻找边界的问题。单调栈的核心思想是"及时处理无用数据,保持结构有序性",这种思想在很多算法问题中都有体现。
9. 不同语言的实现差异
9.1 Java实现
java复制public int largestRectangleArea(int[] heights) {
Stack<Integer> stack = new Stack<>();
int maxArea = 0;
int[] newHeights = new int[heights.length + 2];
System.arraycopy(heights, 0, newHeights, 1, heights.length);
for (int i = 0; i < newHeights.length; i++) {
while (!stack.isEmpty() && newHeights[stack.peek()] > newHeights[i]) {
int h = newHeights[stack.pop()];
int w = i - stack.peek() - 1;
maxArea = Math.max(maxArea, h * w);
}
stack.push(i);
}
return maxArea;
}
9.2 C++实现
cpp复制int largestRectangleArea(vector<int>& heights) {
stack<int> st;
heights.insert(heights.begin(), 0);
heights.push_back(0);
int maxArea = 0;
for (int i = 0; i < heights.size(); i++) {
while (!st.empty() && heights[st.top()] > heights[i]) {
int h = heights[st.top()];
st.pop();
int w = i - st.top() - 1;
maxArea = max(maxArea, h * w);
}
st.push(i);
}
return maxArea;
}
10. 常见错误与调试技巧
- 忘记处理栈空的情况:添加哨兵解决
- 宽度计算错误:注意是i-stack[-1]-1而非i-cur
- 特殊输入处理:空数组或单元素数组
- 调试方法:
- 打印栈状态和关键变量
- 用小案例手动模拟
- 对比暴力解法的结果
实际编码时,建议先用小例子(如[2,1,2])走一遍流程,确保边界条件处理正确。我在最初实现时曾多次因宽度计算错误而得不到正确结果,后来发现关键在于理解右边界是i,左边界是弹出后的栈顶元素。
