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

12 KiB
Raw Blame History

最小栈 (Min Stack)

LeetCode 155. Medium

题目描述

设计一个支持 pushpoptop 操作,并能在常数时间内检索到最小元素的栈。

实现 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]
            ↓  ↓  ↓  ↓
对应关系:  每个位置的辅助栈值记录了"包括该元素及之前所有元素的最小值"

为什么这样思考?

  1. 空间换时间:用 O(n) 额外空间,让 getMin() 达到 O(1)
  2. 同步更新:主栈和辅助栈同步操作,始终保持对应关系
  3. 冗余存储:辅助栈存储历史最小值,即使最小值被弹出也能快速回溯

解题思路

核心思想

双栈方案:使用两个栈

  • 主栈 (stack):存储所有元素
  • 辅助栈 (minStack):存储每个位置对应的最小值

算法流程(详细版)

步骤1初始化

func Constructor() MinStack {
    return MinStack{
        stack:    []int{},     // 主栈
        minStack: []int{},     // 辅助栈
    }
}

步骤2Push 操作

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 操作

func (this *MinStack) Pop() {
    // 同步弹出两个栈的顶部元素
    this.stack = this.stack[:len(this.stack)-1]
    this.minStack = this.minStack[:len(this.minStack)-1]
}

关键点

  • 为什么两个栈都要 pop
    • 保持栈的对应关系
    • 弹出当前元素后,最小值自然回到上一个状态

步骤4Top 操作

func (this *MinStack) Top() int {
    return this.stack[len(this.stack)-1]
}

步骤5GetMin 操作

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

// 错误做法:只 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),但平均情况更优 缺点:实现复杂,容易出错

总结

核心要点

  1. 空间换时间:用辅助栈存储历史最小值
  2. 同步操作:两个栈必须保持高度一致
  3. 冗余存储:辅助栈会重复存储最小值,这是关键

易错点

  • 辅助栈不存储重复的最小值
  • Pop 时忘记同步弹出辅助栈
  • 空栈时未检查直接访问

为什么这样设计?

  • 辅助栈的本质是记录每个状态的最小值
  • 即使最小值被弹出,也能快速回溯到上一个最小值
  • 时间复杂度从 O(n) 优化到 O(1)