1. 问题背景与核心挑战
柱状图最大矩形问题(Largest Rectangle in Histogram)是LeetCode上经典的Hard级别算法题,编号84。给定n个非负整数表示柱状图的高度,每个柱子的宽度为1,求该柱状图中能够勾勒出的最大矩形面积。这个问题在数据可视化、图像处理、城市规划等领域都有实际应用场景。
我第一次遇到这个问题时,暴力解法的时间复杂度是O(n²),这在LeetCode上会导致超时。后来发现单调栈(Monotonic Stack)才是解决这类问题的银弹。单调栈通过维护一个有序的栈结构,可以将时间复杂度优化到O(n),空间复杂度O(n)。
2. 单调栈原理解析
2.1 什么是单调栈
单调栈是一种特殊的栈结构,栈内元素保持单调递增或单调递减的顺序。在解决柱状图问题时,我们通常使用单调递增栈(栈底到栈顶元素值递增)。这种数据结构特别适合处理"寻找下一个更大/更小元素"这类问题。
单调栈的工作机制可以类比排队买票的场景:假设新来的人比队伍末尾的人高,就直接排在后面;如果比末尾的人矮,就让末尾的人出列,直到找到合适的位置。这个过程正好对应了柱状图中矩形边界的确定。
2.2 算法正确性证明
为什么单调栈能解决这个问题?关键在于它能高效地找到每个柱子左右两侧第一个比它矮的柱子(称为边界)。对于柱子i,最大矩形面积就是height[i] × (right_boundary - left_boundary - 1)。
通过维护单调递增栈,当遇到一个比栈顶元素矮的柱子时,栈顶元素找到了它的右边界,而它在栈中的前一个元素就是左边界。这个过程确保了每个柱子入栈和出栈各一次,时间复杂度O(n)。
3. Java实现详解
3.1 基础实现代码
java复制public int largestRectangleArea(int[] heights) {
Stack<Integer> stack = new Stack<>();
int maxArea = 0;
int n = heights.length;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && h < heights[stack.peek()]) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
3.2 关键代码解析
-
哨兵技巧:在遍历时添加一个高度为0的虚拟柱子(i == n时),确保栈中所有元素都能被弹出计算。这避免了单独处理栈中剩余元素。
-
宽度计算:当弹出栈顶元素时,当前索引i是右边界,新的栈顶元素是左边界。宽度计算为i - stack.peek() - 1,如果栈为空则宽度就是i。
-
栈存储索引:栈中存储的是柱子的索引而非高度值,这样既能获取高度信息,又能方便计算宽度。
4. 复杂度分析与优化
4.1 时间复杂度
每个柱子最多入栈和出栈各一次,所以时间复杂度是O(n)。这比暴力解法的O(n²)有了质的飞跃。
4.2 空间复杂度
最坏情况下所有柱子按高度递增顺序入栈,空间复杂度O(n)。实际应用中可以通过数组模拟栈来减少常数项开销:
java复制int[] stack = new int[n+1];
int top = -1;
5. 边界条件与特殊案例
5.1 常见边界情况
- 空输入:应该返回0
- 单柱子:面积就是柱子高度
- 所有柱子等高:面积是height × length
- 严格递增序列:每个柱子宽度为1
- 严格递减序列:需要正确处理栈弹出逻辑
5.2 测试用例设计
java复制@Test
public void testCases() {
assertEquals(10, solution.largestRectangleArea(new int[]{2,1,5,6,2,3}));
assertEquals(4, solution.largestRectangleArea(new int[]{2,4}));
assertEquals(0, solution.largestRectangleArea(new int[]{}));
assertEquals(9, solution.largestRectangleArea(new int[]{1,1,1,1,1,1,1,1,1}));
assertEquals(20, solution.largestRectangleArea(new int[]{3,6,5,7,4,8,1,0}));
}
6. 算法扩展与应用
6.1 二维矩阵中的最大矩形
这个问题可以扩展为85题Maximal Rectangle,在二维二进制矩阵中寻找只包含1的最大矩形。解法是将二维问题转化为一系列柱状图问题,然后使用相同的单调栈方法:
java复制public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) return 0;
int maxArea = 0;
int[] dp = new int[matrix[0].length];
for (char[] row : matrix) {
for (int i = 0; i < row.length; i++) {
dp[i] = row[i] == '1' ? dp[i] + 1 : 0;
}
maxArea = Math.max(maxArea, largestRectangleArea(dp));
}
return maxArea;
}
6.2 实际工程应用
- 数据可视化:自动调整图表布局
- 图像处理:识别最大连通区域
- 城市规划:计算地块最大利用率
- 基因组学:分析DNA序列特征
7. 常见错误与调试技巧
7.1 典型错误模式
- 边界处理不当:忘记处理栈中剩余元素,导致部分矩形未被计算
- 宽度计算错误:在计算宽度时混淆了索引和值的关系
- 哨兵遗漏:没有添加虚拟柱子导致逻辑复杂化
- 栈空判断:弹出元素后未检查栈是否为空
7.2 Debug技巧
- 打印栈状态:在每次入栈/出栈时打印当前栈内容
- 可视化追踪:用小例子手工模拟算法执行过程
- 单元测试:针对特殊案例编写针对性测试
- 断点调试:在关键逻辑处设置断点观察变量变化
8. 性能优化实战
8.1 栈实现选择
Java的Stack类由于同步开销,性能不如Deque。建议使用ArrayDeque:
java复制Deque<Integer> stack = new ArrayDeque<>();
8.2 预分配数组
对于性能敏感场景,可以预分配数组代替栈:
java复制int[] stack = new int[heights.length + 1];
int top = -1;
stack[++top] = -1; // 哨兵
for (int i = 0; i <= heights.length; i++) {
int h = (i == heights.length) ? 0 : heights[i];
while (top > 0 && h < heights[stack[top]]) {
int height = heights[stack[top--]];
int width = i - stack[top] - 1;
maxArea = Math.max(maxArea, height * width);
}
stack[++top] = i;
}
8.3 并行计算优化
对于超大数组,可以考虑将柱状图分段后并行计算,最后合并结果。但要注意分段点的处理需要额外逻辑。
9. 算法对比与替代方案
9.1 分治法
虽然分治法也能解决这个问题,时间复杂度O(nlogn),但实际性能不如单调栈:
java复制public int calculateArea(int[] heights, int start, int end) {
if (start > end) return 0;
int minIndex = start;
for (int i = start; i <= end; i++) {
if (heights[i] < heights[minIndex]) minIndex = i;
}
return Math.max(heights[minIndex] * (end - start + 1),
Math.max(calculateArea(heights, start, minIndex - 1),
calculateArea(heights, minIndex + 1, end)));
}
9.2 线段树优化
可以使用线段树快速查询区间最小值,将分治法优化到O(nlogn),但实现复杂度高,通常不如单调栈实用。
10. 面试技巧与解题模板
10.1 面试应答策略
- 先阐述暴力解法,分析其不足
- 引入单调栈概念,解释其优化原理
- 重点说明哨兵技巧和边界处理
- 讨论时间/空间复杂度
- 提及可能的扩展应用
10.2 解题模板
java复制public int template(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>();
int max = 0;
for (int i = 0; i <= heights.length; i++) {
int h = (i == heights.length) ? 0 : heights[i];
while (!stack.isEmpty() && h < heights[stack.peek()]) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
max = Math.max(max, height * width);
}
stack.push(i);
}
return max;
}
这个模板适用于大多数单调栈问题,只需根据具体问题调整比较条件和面积计算方式。