Files
interview/16-LeetCode Hot 100/最小栈.md
yasinshaw 67189941d8 docs: 改进 LeetCode 题目解题思路详细程度
改进以下三个题目的文档:
1. 最小栈 (LeetCode 155)
2. 最大正方形 (LeetCode 221)
3. 柱状图中最大的矩形 (LeetCode 84)

改进内容:
- 新增"思路推导"部分:从暴力解法分析开始,逐步优化
- 详细化"解题思路"部分:分步骤说明,增加 Q&A 问答
- 新增"关键细节说明":解释为什么这样写代码
- 新增"边界条件分析":覆盖各种特殊情况
- 新增"执行过程演示":完整示例跟踪
- 新增"常见错误":对比错误和正确写法
- 新增"进阶问题":扩展思路

参考文档:算法解题思路改进方案.md

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:32:25 +08:00

529 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 最小栈 (Min Stack)
LeetCode 155. Medium
## 题目描述
设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。
实现 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{}, // 辅助栈
}
}
```
**步骤2Push 操作**
```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
- 记录历史信息,方便回溯
**步骤3Pop 操作**
```go
func (this *MinStack) Pop() {
// 同步弹出两个栈的顶部元素
this.stack = this.stack[:len(this.stack)-1]
this.minStack = this.minStack[:len(this.minStack)-1]
}
```
**关键点**
- 为什么两个栈都要 pop
- 保持栈的对应关系
- 弹出当前元素后,最小值自然回到上一个状态
**步骤4Top 操作**
```go
func (this *MinStack) Top() int {
return this.stack[len(this.stack)-1]
}
```
**步骤5GetMin 操作**
```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
- 如果辅助栈存的是 4Pop 后就找不到 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)