# 最小栈 (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)