改进以下三个题目的文档: 1. 最小栈 (LeetCode 155) 2. 最大正方形 (LeetCode 221) 3. 柱状图中最大的矩形 (LeetCode 84) 改进内容: - 新增"思路推导"部分:从暴力解法分析开始,逐步优化 - 详细化"解题思路"部分:分步骤说明,增加 Q&A 问答 - 新增"关键细节说明":解释为什么这样写代码 - 新增"边界条件分析":覆盖各种特殊情况 - 新增"执行过程演示":完整示例跟踪 - 新增"常见错误":对比错误和正确写法 - 新增"进阶问题":扩展思路 参考文档:算法解题思路改进方案.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
896 lines
19 KiB
Markdown
896 lines
19 KiB
Markdown
# 柱状图中最大的矩形 (Largest Rectangle in Histogram)
|
||
|
||
LeetCode 84. Hard
|
||
|
||
## 题目描述
|
||
|
||
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。
|
||
|
||
求在该柱状图中,能够勾勒出来的矩形的最大面积。
|
||
|
||
**示例 1**:
|
||
```
|
||
输入:heights = [2,1,5,6,2,3]
|
||
输出:10
|
||
解释:最大的矩形面积为 10(由高度为 5 和 6 的两个柱子组成)
|
||
```
|
||
|
||
**示例 2**:
|
||
```
|
||
输入:heights = [2,4]
|
||
输出:4
|
||
```
|
||
|
||
## 思路推导
|
||
|
||
### 暴力解法分析
|
||
|
||
**最直观的思路**:枚举所有可能的矩形
|
||
|
||
```go
|
||
func largestRectangleArea(heights []int) int {
|
||
n := len(heights)
|
||
maxArea := 0
|
||
|
||
// 枚举每个柱子作为矩形的高度
|
||
for i := 0; i < n; i++ {
|
||
height := heights[i]
|
||
|
||
// 向左右扩展,找到能形成的最大宽度
|
||
left := i
|
||
for left > 0 && heights[left-1] >= height {
|
||
left--
|
||
}
|
||
|
||
right := i
|
||
for right < n-1 && heights[right+1] >= height {
|
||
right++
|
||
}
|
||
|
||
width := right - left + 1
|
||
area := height * width
|
||
maxArea = max(maxArea, area)
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
**时间复杂度分析**:
|
||
```
|
||
- 外层循环:O(n) 枚举每个柱子
|
||
- 向左扩展:O(n) 最坏情况遍历所有柱子
|
||
- 向右扩展:O(n) 最坏情况遍历所有柱子
|
||
- 总计:O(n) × O(n) = O(n²)
|
||
```
|
||
|
||
**问题**:
|
||
- 对于严格递增的数组 [1,2,3,4,5],每次都要扩展到边界
|
||
- 时间复杂度 O(n²),大型数组会超时
|
||
|
||
### 优化思考
|
||
|
||
**观察**:暴力解法中,很多比较是重复的。
|
||
|
||
**关键问题**:能否避免重复比较,快速找到每个柱子的边界?
|
||
|
||
**思路1:预处理 - 预先计算左右边界**
|
||
|
||
```go
|
||
// left[i]:柱子 i 向左能扩展到的最远位置
|
||
// right[i]:柱子 i 向右能扩展到的最远位置
|
||
|
||
func largestRectangleArea(heights []int) int {
|
||
n := len(heights)
|
||
left := make([]int, n)
|
||
right := make([]int, n)
|
||
|
||
for i := 0; i < n; i++ {
|
||
left[i] = i
|
||
for left[i] > 0 && heights[left[i]-1] >= heights[i] {
|
||
left[i]--
|
||
}
|
||
}
|
||
|
||
for i := n - 1; i >= 0; i-- {
|
||
right[i] = i
|
||
for right[i] < n-1 && heights[right[i]+1] >= heights[i] {
|
||
right[i]++
|
||
}
|
||
}
|
||
|
||
maxArea := 0
|
||
for i := 0; i < n; i++ {
|
||
area := heights[i] * (right[i] - left[i] + 1)
|
||
maxArea = max(maxArea, area)
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
**问题**:仍然是 O(n²),没有本质优化
|
||
|
||
**思路2:单调栈优化** ✅
|
||
|
||
核心洞察:**利用单调递增栈,可以在 O(1) 时间内确定每个柱子的右边界**
|
||
|
||
### 为什么这样思考?
|
||
|
||
**关键观察**:
|
||
```
|
||
对于柱子 i,它的右边界是:
|
||
第一个高度小于 heights[i] 的柱子的位置 - 1
|
||
|
||
例如:[2, 1, 5, 6, 2, 3]
|
||
↑
|
||
柱子 2(高度 6)
|
||
右边界是柱子 4(高度 2)的位置 - 1 = 3
|
||
因为 heights[4] = 2 < 6
|
||
```
|
||
|
||
**单调栈的作用**:
|
||
- **维护一个递增的序列**:栈中柱子的高度严格递增
|
||
- **快速找到边界**:当遇到比栈顶小的柱子时,就找到了栈顶元素的右边界
|
||
- **避免重复比较**:每个柱子最多入栈一次、出栈一次
|
||
|
||
**为什么是单调递增?**
|
||
- 递增保证了:栈顶元素的右边界就是当前遇到的第一个更小的柱子
|
||
- 如果栈中元素不递增,就无法保证这一点
|
||
|
||
## 解题思路
|
||
|
||
### 核心思想
|
||
|
||
**单调递增栈**:
|
||
- 栈中存储柱子的**索引**(不是高度)
|
||
- 栈中对应的柱子高度**严格递增**
|
||
- 当遇到比栈顶小的柱子时,弹出栈顶并计算面积
|
||
|
||
### 算法流程(详细版)
|
||
|
||
**步骤1:初始化栈和遍历**
|
||
|
||
```go
|
||
stack := []int{} // 单调递增栈,存储索引
|
||
maxArea := 0
|
||
n := len(heights)
|
||
|
||
for i := 0; i <= n; i++ { // 注意:遍历到 n(包含虚拟柱子)
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- 为什么遍历到 n(而不是 n-1)?
|
||
- 需要一个"虚拟柱子"高度为 0
|
||
- 强制弹出栈中所有剩余元素并计算面积
|
||
- 否则栈中元素会遗漏计算
|
||
|
||
**步骤2:维护单调递增栈**
|
||
|
||
```go
|
||
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
|
||
// 栈顶元素的高度比当前柱子大
|
||
// 找到了栈顶元素的右边界(当前柱子 i)
|
||
|
||
height := heights[stack[len(stack)-1]]
|
||
stack = stack[:len(stack)-1] // 弹出栈顶
|
||
|
||
// 计算宽度
|
||
width := i
|
||
if len(stack) > 0 {
|
||
width = i - stack[len(stack)-1] - 1
|
||
}
|
||
|
||
area := height * width
|
||
maxArea = max(maxArea, area)
|
||
}
|
||
|
||
stack = append(stack, i) // 当前柱子入栈
|
||
```
|
||
|
||
**关键点解释**:
|
||
|
||
**Q1:为什么条件是 `h < heights[stack[len(stack)-1]]`?**
|
||
|
||
```
|
||
当遇到比栈顶小的柱子时:
|
||
- 栈顶元素的右边界确定了(当前柱子 i 的位置)
|
||
- 因为当前柱子是第一个比栈顶小的柱子
|
||
- 弹出栈顶并计算面积
|
||
|
||
示例:[2, 1, 5, 6, 2, 3]
|
||
↑
|
||
i=4, h=2
|
||
|
||
栈:[2, 3](索引,对应高度 [5, 6])
|
||
|
||
比较:h(2) < heights[3](6) ✓
|
||
- 弹出索引 3(高度 6)
|
||
- 右边界 = 4 - 1 = 3
|
||
- 计算面积:6 × 宽度
|
||
```
|
||
|
||
**Q2:宽度如何计算?**
|
||
|
||
```
|
||
情况1:栈为空
|
||
width = i
|
||
原因:弹出后栈为空,说明这是最小的柱子,可以扩展到最左边
|
||
|
||
情况2:栈不为空
|
||
width = i - stack[len(stack)-1] - 1
|
||
原因:
|
||
- stack[len(stack)-1] 是弹出后新的栈顶
|
||
- 新栈顶的右边界 + 1 到 i-1 是弹出的柱子能扩展的范围
|
||
- 减 1 是因为不包括新栈顶的位置
|
||
|
||
示例:[2, 1, 5, 6, 2, 3]
|
||
索引: 0 1 2 3 4 5
|
||
|
||
弹出索引 3(高度 6)后:
|
||
- 栈变为 [2](索引 2,高度 5)
|
||
- width = 4 - 2 - 1 = 1
|
||
- 只有索引 3 这一个柱子能形成高度为 6 的矩形
|
||
```
|
||
|
||
**Q3:为什么最后要入栈?**
|
||
|
||
```go
|
||
stack = append(stack, i)
|
||
```
|
||
|
||
```
|
||
当前柱子入栈的原因:
|
||
1. 可能是未来某个柱子的左边界
|
||
2. 还没有找到它的右边界
|
||
3. 保持单调性,等待后续处理
|
||
```
|
||
|
||
**步骤3:完整流程示例**
|
||
|
||
```
|
||
输入:[2, 1, 5, 6, 2, 3]
|
||
索引: 0 1 2 3 4 5
|
||
|
||
i=0, h=2:
|
||
栈:[] → [0]
|
||
(空栈,直接入栈)
|
||
|
||
i=1, h=1:
|
||
栈:[0]
|
||
比较:1 < heights[0](2) ✓
|
||
弹出 0:
|
||
height = 2
|
||
width = 1(栈为空)
|
||
area = 2 × 1 = 2
|
||
栈:[] → [1]
|
||
|
||
i=2, h=5:
|
||
栈:[1]
|
||
比较:5 > heights[1](1) ✗
|
||
直接入栈
|
||
栈:[1, 2]
|
||
|
||
i=3, h=6:
|
||
栈:[1, 2]
|
||
比较:6 > heights[2](5) ✗
|
||
直接入栈
|
||
栈:[1, 2, 3]
|
||
|
||
i=4, h=2:
|
||
栈:[1, 2, 3]
|
||
比较:2 < heights[3](6) ✓
|
||
弹出 3:
|
||
height = 6
|
||
width = 4 - 2 - 1 = 1
|
||
area = 6 × 1 = 6
|
||
|
||
比较:2 < heights[2](5) ✓
|
||
弹出 2:
|
||
height = 5
|
||
width = 4 - 1 - 1 = 2
|
||
area = 5 × 2 = 10 ← 当前最大
|
||
|
||
比较:2 > heights[1](1) ✗
|
||
停止弹出
|
||
栈:[1] → [1, 4]
|
||
|
||
i=5, h=3:
|
||
栈:[1, 4]
|
||
比较:3 > heights[4](2) ✗
|
||
直接入栈
|
||
栈:[1, 4, 5]
|
||
|
||
i=6, h=0(虚拟柱子):
|
||
栈:[1, 4, 5]
|
||
依次弹出所有元素并计算...
|
||
|
||
最大面积:10
|
||
```
|
||
|
||
### 关键细节说明
|
||
|
||
**细节1:为什么需要虚拟柱子?**
|
||
|
||
```go
|
||
for i := 0; i <= n; i++ { // 注意 i <= n
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
```
|
||
没有虚拟柱子的问题:
|
||
输入:[2, 3, 4](严格递增)
|
||
栈:[0, 1, 2]
|
||
|
||
遍历结束后,栈中还有 [0, 1, 2]
|
||
这些元素没有找到右边界!
|
||
需要额外处理剩余元素
|
||
|
||
有虚拟柱子的好处:
|
||
i=3, h=0 会强制弹出所有元素
|
||
无需额外处理
|
||
```
|
||
|
||
**细节2:为什么栈中存储索引而不是高度?**
|
||
|
||
```go
|
||
stack := []int{} // 存储索引
|
||
// 而不是 []int{heights[0], heights[1], ...}
|
||
```
|
||
|
||
```
|
||
存储索引的原因:
|
||
1. 需要通过索引计算宽度
|
||
2. 索引是唯一的,高度可能重复
|
||
3. 方便确定位置关系
|
||
|
||
示例:[2, 2, 2]
|
||
如果存储高度:栈 = [2, 2, 2] 无法区分
|
||
如果存储索引:栈 = [0, 1, 2] 清晰明确
|
||
```
|
||
|
||
**细节3:为什么是严格递增而不是非递减?**
|
||
|
||
```go
|
||
// 条件:h < heights[stack[len(stack)-1]]
|
||
// 使用 < 而不是 <=
|
||
|
||
// 如果使用 <= 会怎样?
|
||
for len(stack) > 0 && h <= heights[stack[len(stack)-1]] {
|
||
// 会弹出相等的元素
|
||
}
|
||
```
|
||
|
||
```
|
||
使用 < 的原因:
|
||
- 相等高度的柱子可以形成更大的矩形
|
||
- 弹出相等的元素会错过可能的更大面积
|
||
|
||
示例:[2, 2]
|
||
使用 <:栈 = [0, 1]
|
||
i=2(虚拟柱子)弹出 1, 0
|
||
最大面积 = 2 × 2 = 4 ✓
|
||
|
||
使用 <=:i=1 弹出 0(错误!)
|
||
栈 = [1]
|
||
最大面积 = 2 × 1 = 2 ✗
|
||
```
|
||
|
||
### 边界条件分析
|
||
|
||
**边界1:空数组**
|
||
```
|
||
输入:heights = []
|
||
输出:0
|
||
处理:循环不执行,maxArea = 0
|
||
```
|
||
|
||
**边界2:单个元素**
|
||
```
|
||
输入:heights = [5]
|
||
输出:5
|
||
过程:
|
||
i=0: 栈 = [0]
|
||
i=1: 弹出 0,area = 5 × 1 = 5
|
||
maxArea = 5 ✓
|
||
```
|
||
|
||
**边界3:所有元素相同**
|
||
```
|
||
输入:heights = [3, 3, 3]
|
||
输出:9
|
||
过程:
|
||
i=0: 栈 = [0]
|
||
i=1: 栈 = [0, 1]
|
||
i=2: 栈 = [0, 1, 2]
|
||
i=3: 依次弹出
|
||
弹出 2: area = 3 × 1 = 3
|
||
弹出 1: area = 3 × 2 = 6
|
||
弹出 0: area = 3 × 3 = 9 ✓
|
||
maxArea = 9
|
||
```
|
||
|
||
**边界4:严格递增**
|
||
```
|
||
输入:heights = [1, 2, 3, 4]
|
||
输出:6(不是 10!)
|
||
过程:
|
||
最大面积是 3×2=6(高度 3,宽度 2)
|
||
或者 2×3=6(高度 2,宽度 3)
|
||
|
||
无法形成 4×4 的矩形,因为高度不够
|
||
```
|
||
|
||
**边界5:严格递减**
|
||
```
|
||
输入:heights = [4, 3, 2, 1]
|
||
输出:6
|
||
过程:
|
||
每个柱子都会立即弹出
|
||
最大面积 = 4 × 1 = 4
|
||
或者 3 × 2 = 6(高度 3,从索引 1 到 2)
|
||
```
|
||
|
||
### 复杂度分析(详细版)
|
||
|
||
**时间复杂度**:
|
||
```
|
||
- 外层循环:O(n+1) ≈ O(n)
|
||
- 内层 while 循环:看似 O(n),实际上总计 O(n)
|
||
|
||
为什么内层循环总计 O(n)?
|
||
- 每个索引最多入栈 1 次
|
||
- 每个索引最多出栈 1 次
|
||
- 总操作次数 = 2n = O(n)
|
||
|
||
总计:O(n) + O(n) = O(n)
|
||
```
|
||
|
||
**空间复杂度**:
|
||
```
|
||
- 栈空间:O(n) 最坏情况
|
||
- 其他变量:O(1)
|
||
- 总计:O(n)
|
||
|
||
最坏情况:严格递增数组
|
||
栈会存储所有索引
|
||
```
|
||
|
||
## 执行过程演示
|
||
|
||
**示例**:heights = [2, 1, 5, 6, 2, 3]
|
||
|
||
```
|
||
图示:
|
||
█
|
||
█ █
|
||
█ █ █
|
||
█ █ █ █ █
|
||
█ █ █ █ █
|
||
0 1 2 3 4 5
|
||
|
||
步骤详解:
|
||
|
||
Step 0: 初始化
|
||
stack = []
|
||
maxArea = 0
|
||
|
||
Step 1: i=0, heights[0]=2
|
||
stack = [] → [0]
|
||
(空栈,直接入栈)
|
||
|
||
Step 2: i=1, heights[1]=1
|
||
stack = [0]
|
||
弹出 0(高度 2):
|
||
宽度 = 1(栈为空)
|
||
面积 = 2 × 1 = 2
|
||
stack = [1]
|
||
maxArea = 2
|
||
|
||
Step 3: i=2, heights[2]=5
|
||
stack = [1] → [1, 2]
|
||
(5 > 1,直接入栈)
|
||
|
||
Step 4: i=3, heights[3]=6
|
||
stack = [1, 2] → [1, 2, 3]
|
||
(6 > 5,直接入栈)
|
||
|
||
Step 5: i=4, heights[4]=2
|
||
stack = [1, 2, 3]
|
||
|
||
弹出 3(高度 6):
|
||
宽度 = 4 - 2 - 1 = 1
|
||
面积 = 6 × 1 = 6
|
||
|
||
弹出 2(高度 5):
|
||
宽度 = 4 - 1 - 1 = 2
|
||
面积 = 5 × 2 = 10 ← 最大
|
||
|
||
stack = [1] → [1, 4]
|
||
maxArea = 10
|
||
|
||
Step 6: i=5, heights[5]=3
|
||
stack = [1, 4] → [1, 4, 5]
|
||
(3 > 2,直接入栈)
|
||
|
||
Step 7: i=6(虚拟柱子,h=0)
|
||
stack = [1, 4, 5]
|
||
|
||
弹出 5(高度 3):
|
||
宽度 = 6 - 4 - 1 = 1
|
||
面积 = 3 × 1 = 3
|
||
|
||
弹出 4(高度 2):
|
||
宽度 = 6 - 1 - 1 = 4
|
||
面积 = 2 × 4 = 8
|
||
|
||
弹出 1(高度 1):
|
||
宽度 = 6(栈为空)
|
||
面积 = 1 × 6 = 6
|
||
|
||
stack = []
|
||
maxArea = 10(不变)
|
||
|
||
最终结果:10
|
||
```
|
||
|
||
## 代码实现
|
||
|
||
### Go 实现
|
||
|
||
```go
|
||
func largestRectangleArea(heights []int) int {
|
||
stack := []int{} // 单调递增栈,存储索引
|
||
maxArea := 0
|
||
n := len(heights)
|
||
|
||
// 遍历所有柱子,包括一个虚拟的末尾柱子(高度为 0)
|
||
for i := 0; i <= n; i++ {
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
|
||
// 当前柱子比栈顶小,弹出栈顶并计算面积
|
||
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
|
||
height := heights[stack[len(stack)-1]]
|
||
stack = stack[:len(stack)-1] // 弹出栈顶
|
||
|
||
// 计算宽度
|
||
width := i
|
||
if len(stack) > 0 {
|
||
width = i - stack[len(stack)-1] - 1
|
||
}
|
||
|
||
area := height * width
|
||
if area > maxArea {
|
||
maxArea = area
|
||
}
|
||
}
|
||
|
||
// 当前柱子入栈
|
||
stack = append(stack, i)
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
### 带详细注释的版本
|
||
|
||
```go
|
||
func largestRectangleArea(heights []int) int {
|
||
// 单调递增栈:存储柱子的索引
|
||
// 栈中对应的高度严格递增
|
||
stack := []int{}
|
||
|
||
maxArea := 0
|
||
n := len(heights)
|
||
|
||
// 遍历到 n(包含虚拟柱子)
|
||
for i := 0; i <= n; i++ {
|
||
// 当前柱子的高度
|
||
// 虚拟柱子的高度为 0,用于清空栈
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
|
||
// 维护单调递增栈
|
||
// 如果当前柱子比栈顶小,说明找到了栈顶的右边界
|
||
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
|
||
// 弹出栈顶,计算以该柱子为高的最大矩形
|
||
top := stack[len(stack)-1]
|
||
height := heights[top]
|
||
stack = stack[:len(stack)-1]
|
||
|
||
// 计算宽度
|
||
// 如果栈为空,说明当前是最小的柱子,可以扩展到最左边
|
||
// 如果栈不为空,宽度 = 当前位置 - 新栈顶位置 - 1
|
||
width := i
|
||
if len(stack) > 0 {
|
||
width = i - stack[len(stack)-1] - 1
|
||
}
|
||
|
||
// 计算面积并更新最大值
|
||
area := height * width
|
||
if area > maxArea {
|
||
maxArea = area
|
||
}
|
||
}
|
||
|
||
// 当前柱子入栈
|
||
stack = append(stack, i)
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
## 常见错误
|
||
|
||
### 错误1:忘记虚拟柱子
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
for i := 0; i < n; i++ { // ❌ 只遍历到 n-1
|
||
h := heights[i]
|
||
// ...
|
||
}
|
||
// 栈中可能还有元素未处理!
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
for i := 0; i <= n; i++ { // ✓ 遍历到 n
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**原因**:
|
||
- 虚拟柱子强制弹出所有剩余元素
|
||
- 避免栈中元素遗漏计算
|
||
|
||
### 错误2:宽度计算错误
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
width = i - stack[len(stack)-1] // ❌ 没有减 1
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
width = i - stack[len(stack)-1] - 1 // ✓ 正确计算
|
||
```
|
||
|
||
**原因**:
|
||
- stack[len(stack)-1] 是弹出后新的栈顶
|
||
- 新栈顶的位置不包含在宽度范围内
|
||
- 必须减 1
|
||
|
||
### 错误3:比较条件错误
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
for len(stack) > 0 && h <= heights[stack[len(stack)-1]] {
|
||
// ❌ 使用 <= 会弹出相等的元素
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
|
||
// ✓ 只弹出严格更大的元素
|
||
}
|
||
```
|
||
|
||
**原因**:
|
||
- 相等高度的柱子可以合并成更大的矩形
|
||
- 提前弹出会错过最优解
|
||
|
||
### 错误4:忘记处理空栈情况
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
width = i - stack[len(stack)-1] - 1 // ❌ 空栈会越界
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
width := i
|
||
if len(stack) > 0 {
|
||
width = i - stack[len(stack)-1] - 1
|
||
}
|
||
```
|
||
|
||
**原因**:
|
||
- 空栈说明当前是最小的柱子
|
||
- 可以扩展到最左边(索引 0)
|
||
|
||
## 进阶问题
|
||
|
||
### Q1:空间复杂度能否优化?
|
||
|
||
**答案**:不能,单调栈本身就需要 O(n) 空间。
|
||
|
||
但是可以用数组模拟栈,避免动态分配:
|
||
```go
|
||
func largestRectangleArea(heights []int) int {
|
||
n := len(heights)
|
||
stack := make([]int, n+1) // 预分配
|
||
top := -1 // 栈顶指针
|
||
|
||
maxArea := 0
|
||
|
||
for i := 0; i <= n; i++ {
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
|
||
for top >= 0 && h < heights[stack[top]] {
|
||
height := heights[stack[top]]
|
||
top--
|
||
|
||
width := i
|
||
if top >= 0 {
|
||
width = i - stack[top] - 1
|
||
}
|
||
|
||
area := height * width
|
||
if area > maxArea {
|
||
maxArea = area
|
||
}
|
||
}
|
||
|
||
top++
|
||
stack[top] = i
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
### Q2:如何找到最大矩形的位置?
|
||
|
||
**方案**:记录最大矩形的左右边界
|
||
|
||
```go
|
||
func largestRectangleAreaWithPosition(heights []int) (int, int, int) {
|
||
stack := []int{}
|
||
maxArea := 0
|
||
left, right := -1, -1
|
||
n := len(heights)
|
||
|
||
for i := 0; i <= n; i++ {
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
|
||
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
|
||
top := stack[len(stack)-1]
|
||
height := heights[top]
|
||
stack = stack[:len(stack)-1]
|
||
|
||
l := 0
|
||
if len(stack) > 0 {
|
||
l = stack[len(stack)-1] + 1
|
||
}
|
||
r := i - 1
|
||
|
||
area := height * (r - l + 1)
|
||
if area > maxArea {
|
||
maxArea = area
|
||
left, right = l, r
|
||
}
|
||
}
|
||
|
||
stack = append(stack, i)
|
||
}
|
||
|
||
return maxArea, left, right
|
||
}
|
||
```
|
||
|
||
### Q3:这道题和"接雨水"有什么区别?
|
||
|
||
**接雨水(LeetCode 42)**:
|
||
- 找左右两边**更大**的柱子
|
||
- 使用单调**递减**栈
|
||
- 计算的是"凹槽"的面积
|
||
|
||
**最大矩形**:
|
||
- 找左右边**更小**的柱子
|
||
- 使用单调**递增**栈
|
||
- 计算的是矩形面积
|
||
|
||
### Q4:如何处理高度非常大的情况?
|
||
|
||
**答案**:使用 64 位整数避免溢出
|
||
|
||
```go
|
||
func largestRectangleArea(heights []int) int64 {
|
||
stack := []int{}
|
||
var maxArea int64 = 0
|
||
n := len(heights)
|
||
|
||
for i := 0; i <= n; i++ {
|
||
h := 0
|
||
if i < n {
|
||
h = heights[i]
|
||
}
|
||
|
||
for len(stack) > 0 && h < heights[stack[len(stack)-1]] {
|
||
height := int64(heights[stack[len(stack)-1]])
|
||
stack = stack[:len(stack)-1]
|
||
|
||
var width int64 = int64(i)
|
||
if len(stack) > 0 {
|
||
width = int64(i - stack[len(stack)-1] - 1)
|
||
}
|
||
|
||
area := height * width
|
||
if area > maxArea {
|
||
maxArea = area
|
||
}
|
||
}
|
||
|
||
stack = append(stack, i)
|
||
}
|
||
|
||
return maxArea
|
||
}
|
||
```
|
||
|
||
## 相关题目
|
||
|
||
1. **LeetCode 85. 最大矩形**
|
||
- 二维版本,每行看作柱状图
|
||
- 先预处理每列的连续 1 的个数
|
||
- 对每行调用本题的解法
|
||
|
||
2. **LeetCode 42. 接雨水**
|
||
- 单调递减栈
|
||
- 找更大的柱子形成凹槽
|
||
|
||
3. **LeetCode 739. 每日温度**
|
||
- 单调递减栈
|
||
- 找下一个更大的元素
|
||
|
||
## 总结
|
||
|
||
**核心要点**:
|
||
1. **单调递增栈**:维护一个递增的索引序列
|
||
2. **右边界确定**:遇到更小的柱子时,栈顶元素的右边界确定
|
||
3. **虚拟柱子**:高度为 0,强制清空栈
|
||
4. **宽度计算**:根据栈的状态动态计算
|
||
|
||
**易错点**:
|
||
- ❌ 忘记虚拟柱子,导致栈中元素未处理
|
||
- ❌ 宽度计算错误(忘记减 1)
|
||
- ❌ 比较条件错误(使用 <= 而不是 <)
|
||
- ❌ 空栈情况未处理
|
||
|
||
**为什么这样思考?**
|
||
- 单调栈本质上是在"延迟计算"
|
||
- 每个柱子入栈后,等待找到它的右边界
|
||
- 一旦找到右边界,立即计算该柱子能形成的最大矩形
|
||
- 每个柱子只处理一次,时间复杂度 O(n)
|
||
- 暴力解法的 O(n²) 优化到 O(n)
|