docs: 改进 LeetCode 题目解题思路详细程度

改进以下三个题目的文档:
1. 最小栈 (LeetCode 155)
2. 最大正方形 (LeetCode 221)
3. 柱状图中最大的矩形 (LeetCode 84)

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:32:25 +08:00
parent a5736a4db7
commit 67189941d8
3 changed files with 2071 additions and 61 deletions

View File

@@ -1,21 +1,303 @@
# 最小栈 (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)**:存储每个位置对应的最小值
## Go 代码
### 算法流程(详细版)
**步骤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
stack []int // 主栈:存储所有元素
minStack []int // 辅助栈:存储每个位置的最小值
}
func Constructor() MinStack {
@@ -26,7 +308,63 @@ func Constructor() MinStack {
}
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 {
@@ -34,23 +372,157 @@ func (this *MinStack) Push(val int) {
if val < min {
this.minStack = append(this.minStack, val)
} else {
this.minStack = append(this.minStack, min)
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]
this.minStack = this.minStack[:len(this.minStack)-1]
}
func (this *MinStack) Top() int {
return this.stack[len(this.stack)-1]
if diff < 0 {
this.minVal = this.minVal - diff // 恢复之前的最小值
}
}
func (this *MinStack) GetMin() int {
return this.minStack[len(this.minStack)-1]
return this.minVal
}
```
**复杂度:** 所有操作 O(1) 时间
**优点**:最坏情况空间仍为 O(n),但平均情况更优
**缺点**:实现复杂,容易出错
## 总结
**核心要点**
1. **空间换时间**:用辅助栈存储历史最小值
2. **同步操作**:两个栈必须保持高度一致
3. **冗余存储**:辅助栈会重复存储最小值,这是关键
**易错点**
- ❌ 辅助栈不存储重复的最小值
- ❌ Pop 时忘记同步弹出辅助栈
- ❌ 空栈时未检查直接访问
**为什么这样设计?**
- 辅助栈的本质是**记录每个状态的最小值**
- 即使最小值被弹出,也能快速回溯到上一个最小值
- 时间复杂度从 O(n) 优化到 O(1)