改进以下三个题目的文档: 1. 最小栈 (LeetCode 155) 2. 最大正方形 (LeetCode 221) 3. 柱状图中最大的矩形 (LeetCode 84) 改进内容: - 新增"思路推导"部分:从暴力解法分析开始,逐步优化 - 详细化"解题思路"部分:分步骤说明,增加 Q&A 问答 - 新增"关键细节说明":解释为什么这样写代码 - 新增"边界条件分析":覆盖各种特殊情况 - 新增"执行过程演示":完整示例跟踪 - 新增"常见错误":对比错误和正确写法 - 新增"进阶问题":扩展思路 参考文档:算法解题思路改进方案.md Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
12 KiB
12 KiB
最小栈 (Min Stack)
LeetCode 155. Medium
题目描述
设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
- MinStack() 初始化堆栈对象。
- void push(int val) 将元素val推入堆栈。
- void pop() 删除堆栈顶部的元素。
- int top() 获取堆栈顶部的元素。
- int getMin() 获取堆栈中的最小元素。
思路推导
暴力解法分析
最直观的思路:
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:用一个变量记录最小值
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]
↓ ↓ ↓ ↓
对应关系: 每个位置的辅助栈值记录了"包括该元素及之前所有元素的最小值"
为什么这样思考?
- 空间换时间:用 O(n) 额外空间,让 getMin() 达到 O(1)
- 同步更新:主栈和辅助栈同步操作,始终保持对应关系
- 冗余存储:辅助栈存储历史最小值,即使最小值被弹出也能快速回溯
解题思路
核心思想
双栈方案:使用两个栈
- 主栈 (stack):存储所有元素
- 辅助栈 (minStack):存储每个位置对应的最小值
算法流程(详细版)
步骤1:初始化
func Constructor() MinStack {
return MinStack{
stack: []int{}, // 主栈
minStack: []int{}, // 辅助栈
}
}
步骤2:Push 操作
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 操作
func (this *MinStack) Pop() {
// 同步弹出两个栈的顶部元素
this.stack = this.stack[:len(this.stack)-1]
this.minStack = this.minStack[:len(this.minStack)-1]
}
关键点:
- 为什么两个栈都要 pop?
- 保持栈的对应关系
- 弹出当前元素后,最小值自然回到上一个状态
步骤4:Top 操作
func (this *MinStack) Top() int {
return this.stack[len(this.stack)-1]
}
步骤5:GetMin 操作
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?
// 错误做法:只 pop 主栈
func (this *MinStack) Pop() {
this.stack = this.stack[:len(this.stack)-1]
// 忘记 pop 辅助栈!
}
// 问题:下次 GetMin() 会返回错误的最小值
细节3:边界条件 - 栈为空时的处理
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 实现
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:辅助栈只存储严格更小的值
❌ 错误写法:
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)
}
// 问题:两个栈高度不一致!
}
✅ 正确写法:
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
❌ 错误写法:
func (this *MinStack) Pop() {
this.stack = this.stack[:len(this.stack)-1]
// 忘记 pop 辅助栈!
}
✅ 正确写法:
func (this *MinStack) Pop() {
this.stack = this.stack[:len(this.stack)-1]
this.minStack = this.minStack[:len(this.minStack)-1] // 同步弹出
}
原因:
- 不同步会导致辅助栈高度与主栈不一致
- 后续 GetMin() 会返回错误的最小值
错误3:空栈未检查
❌ 错误写法:
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)
}
}
✅ 正确写法:
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:能否只用一个栈实现?
方案:栈中同时存储值和当前最小值
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 变化时存储)
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),但平均情况更优 缺点:实现复杂,容易出错
总结
核心要点:
- 空间换时间:用辅助栈存储历史最小值
- 同步操作:两个栈必须保持高度一致
- 冗余存储:辅助栈会重复存储最小值,这是关键
易错点:
- ❌ 辅助栈不存储重复的最小值
- ❌ Pop 时忘记同步弹出辅助栈
- ❌ 空栈时未检查直接访问
为什么这样设计?
- 辅助栈的本质是记录每个状态的最小值
- 即使最小值被弹出,也能快速回溯到上一个最小值
- 时间复杂度从 O(n) 优化到 O(1)