改进以下三个题目的文档: 1. 最小栈 (LeetCode 155) 2. 最大正方形 (LeetCode 221) 3. 柱状图中最大的矩形 (LeetCode 84) 改进内容: - 新增"思路推导"部分:从暴力解法分析开始,逐步优化 - 详细化"解题思路"部分:分步骤说明,增加 Q&A 问答 - 新增"关键细节说明":解释为什么这样写代码 - 新增"边界条件分析":覆盖各种特殊情况 - 新增"执行过程演示":完整示例跟踪 - 新增"常见错误":对比错误和正确写法 - 新增"进阶问题":扩展思路 参考文档:算法解题思路改进方案.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
529 lines
12 KiB
Markdown
529 lines
12 KiB
Markdown
# 最小栈 (Min Stack)
|
||
|
||
LeetCode 155. Medium
|
||
|
||
## 题目描述
|
||
|
||
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
|
||
|
||
实现 MinStack 类:
|
||
- **MinStack()** 初始化堆栈对象。
|
||
- **void push(int val)** 将元素val推入堆栈。
|
||
- **void pop()** 删除堆栈顶部的元素。
|
||
- **int top()** 获取堆栈顶部的元素。
|
||
- **int getMin()** 获取堆栈中的最小元素。
|
||
|
||
## 思路推导
|
||
|
||
### 暴力解法分析
|
||
|
||
**最直观的思路**:
|
||
```go
|
||
type MinStack struct {
|
||
stack []int
|
||
}
|
||
|
||
func (this *MinStack) Push(val int) {
|
||
this.stack = append(this.stack, val)
|
||
}
|
||
|
||
func (this *MinStack) GetMin() int {
|
||
minVal := this.stack[0]
|
||
for _, v := range this.stack {
|
||
if v < minVal {
|
||
minVal = v
|
||
}
|
||
}
|
||
return minVal
|
||
}
|
||
```
|
||
|
||
**时间复杂度分析**:
|
||
- Push: O(1)
|
||
- Pop: O(1)
|
||
- Top: O(1)
|
||
- **GetMin: O(n)** ❌ 需要遍历整个栈
|
||
|
||
**问题**:题目要求 getMin() 必须在 O(1) 时间内完成,暴力解法无法满足。
|
||
|
||
### 优化思考
|
||
|
||
**观察**:GetMin 操作的瓶颈在于需要每次都遍历找最小值。
|
||
|
||
**关键问题**:能否在每次 push 时就记录当前的最小值?
|
||
|
||
**思路1:用一个变量记录最小值**
|
||
```go
|
||
type MinStack struct {
|
||
stack []int
|
||
minVal int
|
||
}
|
||
```
|
||
|
||
**问题**:当最小值被 pop 出栈后,如何快速找到次小值?
|
||
- 例如:stack = [2, 1, 3],minVal = 1
|
||
- pop 后:stack = [2, 3],新的 minVal 应该是 2
|
||
- 但我们不知道次小值是多少,需要重新遍历!
|
||
|
||
**思路2:使用辅助栈记录历史最小值** ✅
|
||
|
||
核心洞察:**每个元素对应一个"当时的最小值"**
|
||
|
||
```
|
||
主栈: [2, 1, 3, 0]
|
||
辅助栈: [2, 1, 1, 0]
|
||
↓ ↓ ↓ ↓
|
||
对应关系: 每个位置的辅助栈值记录了"包括该元素及之前所有元素的最小值"
|
||
```
|
||
|
||
### 为什么这样思考?
|
||
|
||
1. **空间换时间**:用 O(n) 额外空间,让 getMin() 达到 O(1)
|
||
2. **同步更新**:主栈和辅助栈同步操作,始终保持对应关系
|
||
3. **冗余存储**:辅助栈存储历史最小值,即使最小值被弹出也能快速回溯
|
||
|
||
## 解题思路
|
||
|
||
### 核心思想
|
||
|
||
**双栈方案**:使用两个栈
|
||
- **主栈 (stack)**:存储所有元素
|
||
- **辅助栈 (minStack)**:存储每个位置对应的最小值
|
||
|
||
### 算法流程(详细版)
|
||
|
||
**步骤1:初始化**
|
||
```go
|
||
func Constructor() MinStack {
|
||
return MinStack{
|
||
stack: []int{}, // 主栈
|
||
minStack: []int{}, // 辅助栈
|
||
}
|
||
}
|
||
```
|
||
|
||
**步骤2:Push 操作**
|
||
```go
|
||
func (this *MinStack) Push(val int) {
|
||
// 1. 主栈总是推入新值
|
||
this.stack = append(this.stack, val)
|
||
|
||
// 2. 辅助栈推入"当前最小值"
|
||
if len(this.minStack) == 0 {
|
||
// 辅助栈为空,直接推入
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
// 辅助栈不为空,比较当前值和之前的min
|
||
min := this.minStack[len(this.minStack)-1]
|
||
if val < min {
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
// 推入之前的min,保持栈同步
|
||
this.minStack = append(this.minStack, min)
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- 为什么辅助栈要推入 min 而不是只推入更小的值?
|
||
- 保持两个栈高度一致,便于同步 pop
|
||
- 记录历史信息,方便回溯
|
||
|
||
**步骤3:Pop 操作**
|
||
```go
|
||
func (this *MinStack) Pop() {
|
||
// 同步弹出两个栈的顶部元素
|
||
this.stack = this.stack[:len(this.stack)-1]
|
||
this.minStack = this.minStack[:len(this.minStack)-1]
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- 为什么两个栈都要 pop?
|
||
- 保持栈的对应关系
|
||
- 弹出当前元素后,最小值自然回到上一个状态
|
||
|
||
**步骤4:Top 操作**
|
||
```go
|
||
func (this *MinStack) Top() int {
|
||
return this.stack[len(this.stack)-1]
|
||
}
|
||
```
|
||
|
||
**步骤5:GetMin 操作**
|
||
```go
|
||
func (this *MinStack) GetMin() int {
|
||
return this.minStack[len(this.minStack)-1]
|
||
}
|
||
```
|
||
|
||
**关键点**:
|
||
- 为什么是 O(1)?
|
||
- 直接访问辅助栈顶部,无需遍历
|
||
|
||
### 关键细节说明
|
||
|
||
**细节1:为什么辅助栈要重复存储最小值?**
|
||
|
||
```
|
||
操作序列: Push(3), Push(2), Push(1), Push(4)
|
||
|
||
主栈: [3] → [3, 2] → [3, 2, 1] → [3, 2, 1, 4]
|
||
辅助栈: [3] → [3, 2] → [3, 2, 1] → [3, 2, 1, 1]
|
||
↑
|
||
不是4,而是之前的min(1)
|
||
|
||
为什么?
|
||
- Pop(4) 后,min 仍然是 1
|
||
- 如果辅助栈存的是 4,Pop 后就找不到 1 了
|
||
```
|
||
|
||
**细节2:为什么要同步 pop?**
|
||
|
||
```go
|
||
// 错误做法:只 pop 主栈
|
||
func (this *MinStack) Pop() {
|
||
this.stack = this.stack[:len(this.stack)-1]
|
||
// 忘记 pop 辅助栈!
|
||
}
|
||
|
||
// 问题:下次 GetMin() 会返回错误的最小值
|
||
```
|
||
|
||
**细节3:边界条件 - 栈为空时的处理**
|
||
|
||
```go
|
||
func (this *MinStack) Push(val int) {
|
||
this.stack = append(this.stack, val)
|
||
|
||
if len(this.minStack) == 0 { // ✅ 必须检查
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
### 边界条件分析
|
||
|
||
**边界1:空栈调用 GetMin**
|
||
```
|
||
输入: MinStack(), GetMin()
|
||
输出: 不保证(题目假设至少有一个元素才调用)
|
||
```
|
||
|
||
**边界2:重复元素**
|
||
```
|
||
操作: Push(1), Push(1), Push(1)
|
||
主栈: [1, 1, 1]
|
||
辅助栈: [1, 1, 1] // 每个位置都是1
|
||
GetMin(): 1 ✓
|
||
Pop(): 主栈=[1,1], 辅助栈=[1,1]
|
||
GetMin(): 1 ✓
|
||
```
|
||
|
||
**边界3:递减序列**
|
||
```
|
||
操作: Push(3), Push(2), Push(1)
|
||
主栈: [3, 2, 1]
|
||
辅助栈: [3, 2, 1] // 同步递减
|
||
GetMin(): 1 ✓
|
||
Pop(): 主栈=[3,2], 辅助栈=[3,2]
|
||
GetMin(): 2 ✓ // 自动回到次小值
|
||
```
|
||
|
||
### 复杂度分析(详细版)
|
||
|
||
**时间复杂度**:
|
||
```
|
||
- Push: O(1)
|
||
- append操作:O(1) 均摊
|
||
- 比较和赋值:O(1)
|
||
|
||
- Pop: O(1)
|
||
- 切片操作:O(1)
|
||
|
||
- Top: O(1)
|
||
- 访问数组末尾:O(1)
|
||
|
||
- GetMin: O(1)
|
||
- 访问数组末尾:O(1)
|
||
```
|
||
|
||
**空间复杂度**:
|
||
```
|
||
- 主栈:O(n)
|
||
- 辅助栈:O(n)
|
||
- 总计:O(n)
|
||
|
||
为什么是 O(n)?
|
||
- 最坏情况:每个元素都需要在辅助栈中存储
|
||
- 例如:递减序列 [3,2,1],辅助栈也是 [3,2,1]
|
||
```
|
||
|
||
## 执行过程演示
|
||
|
||
```
|
||
操作序列: Push(-2), Push(0), Push(-3), GetMin, Pop, GetMin
|
||
|
||
1. Push(-2)
|
||
主栈: [-2]
|
||
辅助栈: [-2] // 空栈,直接推入
|
||
|
||
2. Push(0)
|
||
主栈: [-2, 0]
|
||
辅助栈: [-2, -2] // -2 < 0,推入 -2
|
||
|
||
3. Push(-3)
|
||
主栈: [-2, 0, -3]
|
||
辅助栈: [-2, -2, -3] // -3 < -2,推入 -3
|
||
|
||
4. GetMin()
|
||
返回: -3 // 辅助栈顶部
|
||
|
||
5. Pop()
|
||
主栈: [-2, 0]
|
||
辅助栈: [-2, -2] // 同步弹出
|
||
|
||
6. GetMin()
|
||
返回: -2 // 自动回到之前的最小值
|
||
```
|
||
|
||
## 代码实现
|
||
|
||
### Go 实现
|
||
|
||
```go
|
||
type MinStack struct {
|
||
stack []int // 主栈:存储所有元素
|
||
minStack []int // 辅助栈:存储每个位置的最小值
|
||
}
|
||
|
||
func Constructor() MinStack {
|
||
return MinStack{
|
||
stack: []int{},
|
||
minStack: []int{},
|
||
}
|
||
}
|
||
|
||
func (this *MinStack) Push(val int) {
|
||
// 主栈总是推入新值
|
||
this.stack = append(this.stack, val)
|
||
|
||
// 辅助栈推入当前最小值
|
||
if len(this.minStack) == 0 {
|
||
// 辅助栈为空,当前值就是最小值
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
// 比较当前值和之前的最小值
|
||
min := this.minStack[len(this.minStack)-1]
|
||
if val < min {
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
// 推入之前的最小值,保持同步
|
||
this.minStack = append(this.minStack, min)
|
||
}
|
||
}
|
||
}
|
||
|
||
func (this *MinStack) Pop() {
|
||
// 同步弹出两个栈
|
||
this.stack = this.stack[:len(this.stack)-1]
|
||
this.minStack = this.minStack[:len(this.minStack)-1]
|
||
}
|
||
|
||
func (this *MinStack) Top() int {
|
||
// 返回主栈顶部
|
||
return this.stack[len(this.stack)-1]
|
||
}
|
||
|
||
func (this *MinStack) GetMin() int {
|
||
// 返回辅助栈顶部(当前最小值)
|
||
return this.minStack[len(this.minStack)-1]
|
||
}
|
||
```
|
||
|
||
## 常见错误
|
||
|
||
### 错误1:辅助栈只存储严格更小的值
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
func (this *MinStack) Push(val int) {
|
||
this.stack = append(this.stack, val)
|
||
|
||
if len(this.minStack) == 0 || val < this.minStack[len(this.minStack)-1] {
|
||
this.minStack = append(this.minStack, val)
|
||
}
|
||
// 问题:两个栈高度不一致!
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
func (this *MinStack) Push(val int) {
|
||
this.stack = append(this.stack, val)
|
||
|
||
if len(this.minStack) == 0 {
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
min := this.minStack[len(this.minStack)-1]
|
||
if val < min {
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
this.minStack = append(this.minStack, min) // 重复存储
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**原因**:
|
||
- 必须保持两个栈同步,才能正确 pop
|
||
- 如果辅助栈高度较小,pop 主栈时会越界
|
||
|
||
### 错误2:忘记同步 pop
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
func (this *MinStack) Pop() {
|
||
this.stack = this.stack[:len(this.stack)-1]
|
||
// 忘记 pop 辅助栈!
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
func (this *MinStack) Pop() {
|
||
this.stack = this.stack[:len(this.stack)-1]
|
||
this.minStack = this.minStack[:len(this.minStack)-1] // 同步弹出
|
||
}
|
||
```
|
||
|
||
**原因**:
|
||
- 不同步会导致辅助栈高度与主栈不一致
|
||
- 后续 GetMin() 会返回错误的最小值
|
||
|
||
### 错误3:空栈未检查
|
||
|
||
❌ **错误写法**:
|
||
```go
|
||
func (this *MinStack) Push(val int) {
|
||
this.stack = append(this.stack, val)
|
||
|
||
min := this.minStack[len(this.minStack)-1] // 可能 panic!
|
||
if val < min {
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
this.minStack = append(this.minStack, min)
|
||
}
|
||
}
|
||
```
|
||
|
||
✅ **正确写法**:
|
||
```go
|
||
func (this *MinStack) Push(val int) {
|
||
this.stack = append(this.stack, val)
|
||
|
||
if len(this.minStack) == 0 { // 先检查
|
||
this.minStack = append(this.minStack, val)
|
||
} else {
|
||
min := this.minStack[len(this.minStack)-1]
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
## 进阶问题
|
||
|
||
### Q1:能否只用一个栈实现?
|
||
|
||
**方案**:栈中同时存储值和当前最小值
|
||
|
||
```go
|
||
type MinStack struct {
|
||
stack []pair
|
||
}
|
||
|
||
type pair struct {
|
||
val int
|
||
min int // 当前的最小值
|
||
}
|
||
|
||
func (this *MinStack) Push(val int) {
|
||
min := val
|
||
if len(this.stack) > 0 {
|
||
prevMin := this.stack[len(this.stack)-1].min
|
||
if val > prevMin {
|
||
min = prevMin
|
||
}
|
||
}
|
||
this.stack = append(this.stack, pair{val, min})
|
||
}
|
||
|
||
func (this *MinStack) GetMin() int {
|
||
return this.stack[len(this.stack)-1].min
|
||
}
|
||
```
|
||
|
||
**优点**:逻辑更集中
|
||
**缺点**:每个元素需要额外存储 min(空间开销相同)
|
||
|
||
### Q2:如果要求空间复杂度优化呢?
|
||
|
||
**方案**:差值法(只在 min 变化时存储)
|
||
|
||
```go
|
||
type MinStack struct {
|
||
stack []int
|
||
minVal int
|
||
}
|
||
|
||
func (this *MinStack) Push(val int) {
|
||
if len(this.stack) == 0 {
|
||
this.stack = append(this.stack, 0)
|
||
this.minVal = val
|
||
} else {
|
||
// 存储差值
|
||
diff := val - this.minVal
|
||
this.stack = append(this.stack, diff)
|
||
if diff < 0 {
|
||
this.minVal = val // 更新最小值
|
||
}
|
||
}
|
||
}
|
||
|
||
func (this *MinStack) Pop() {
|
||
diff := this.stack[len(this.stack)-1]
|
||
this.stack = this.stack[:len(this.stack)-1]
|
||
|
||
if diff < 0 {
|
||
this.minVal = this.minVal - diff // 恢复之前的最小值
|
||
}
|
||
}
|
||
|
||
func (this *MinStack) GetMin() int {
|
||
return this.minVal
|
||
}
|
||
```
|
||
|
||
**优点**:最坏情况空间仍为 O(n),但平均情况更优
|
||
**缺点**:实现复杂,容易出错
|
||
|
||
## 总结
|
||
|
||
**核心要点**:
|
||
1. **空间换时间**:用辅助栈存储历史最小值
|
||
2. **同步操作**:两个栈必须保持高度一致
|
||
3. **冗余存储**:辅助栈会重复存储最小值,这是关键
|
||
|
||
**易错点**:
|
||
- ❌ 辅助栈不存储重复的最小值
|
||
- ❌ Pop 时忘记同步弹出辅助栈
|
||
- ❌ 空栈时未检查直接访问
|
||
|
||
**为什么这样设计?**
|
||
- 辅助栈的本质是**记录每个状态的最小值**
|
||
- 即使最小值被弹出,也能快速回溯到上一个最小值
|
||
- 时间复杂度从 O(n) 优化到 O(1)
|