155. 最小栈(Min Stack)

难度:中等 | 主题:栈、设计

题目

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

实现 MinStack 类:

  • MinStack() 初始化堆栈对象

  • void push(int val) 将元素压入堆栈

  • void pop() 删除堆栈顶部的元素

  • int top() 获取堆栈顶部的元素

  • int getMin() 获取堆栈中的最小元素

示例:

 
输入:["MinStack","push","push","push","getMin","pop","top","getMin"]
 
     [[],[-2],[0],[-3],[],[],[],[]]
 
输出:[null,null,null,null,-3,null,0,-2]
 

思路

先讲个故事:两张纸记账法

假设你是学生会的财务,管着一个”费用报销栈”——每笔支出压一张纸在桌上,最后放的在最上面。

突然主席问你:“到现在为止,最小的一笔支出是多少?”

你不想每次数一遍所有的纸。所以你在旁边另拿一张纸,每次有新支出时,记下”到目前为止的最小值”——两张纸一起压上去。

主席问的时候你瞄一眼第二摞纸的最上面那张就行了。

这就是辅助栈的思想:用额外空间换取 O(1) 的最小值查询


引导式推导:从需求到设计

问题本质:普通栈的 getMin 需要遍历所有元素 → O(n)。怎么做到 O(1)?

思路 1:每次 push 时记录当前最小值

  • 主栈 stack:正常存所有元素

  • 辅助栈 min_stack:存每个状态下的最小值,与主栈同步

为什么辅助栈的栈顶等于当前最小值?

因为每次 push 时,辅助栈入栈的是 min(val, 当前最小值)。这意味着:

  • 辅助栈的栈顶始终是”从栈底到当前位置”的最小值

  • 两个栈同进同出,高度永远一致

 
push -2:  stack=[-2]       min_stack=[-2]
 
push  0:  stack=[-2,0]     min_stack=[-2,-2]   # min(0, -2) = -2
 
push -3:  stack=[-2,0,-3]  min_stack=[-2,-2,-3] # min(-3, -2) = -3
 
getMin → min_stack[-1] = -3 ✓
 
pop:      stack=[-2,0]     min_stack=[-2,-2]
 
getMin → min_stack[-1] = -2 ✓
 

graph TD

    subgraph 主栈stack

        S1["-2"]

        S2["0"]

        S3["-3"]

    end

    subgraph 辅助栈min_stack

        M1["-2<br/>min(-2) = -2"]

        M2["-2<br/>min(-2,0) = -2"]

        M3["-3<br/>min(-2,0,-3) = -3"]

    end

    S1 --- M1

    S2 --- M2

    S3 --- M3

    style M3 fill:#f96,stroke:#333


优化方向

辅助栈可以更省空间:只在 val ≤ 当前最小值时入栈,出栈时判断 pop 的值是否等于最小值,是则辅助栈也 pop。

但实践中同步辅助栈法更直观,不易出错。


推荐写法

 
class MinStack:
 
    def __init__(self):
 
        self.stack = []          # 主栈,正常存储所有元素
 
        self.min_stack = []      # 辅助栈,同步维护每个状态的最小值
 
    def push(self, val):
 
        self.stack.append(val)
 
        if not self.min_stack:   # 辅助栈为空,直接入栈
 
            self.min_stack.append(val)
 
        else:
 
            # 比较当前值与辅助栈栈顶,取较小值入辅助栈
 
            self.min_stack.append(min(val, self.min_stack[-1]))
 
    def pop(self):
 
        self.stack.pop()         # 两栈同步出栈
 
        self.min_stack.pop()
 
    def top(self):
 
        return self.stack[-1]    # 返回主栈栈顶
 
    def getMin(self):
 
        return self.min_stack[-1] # 辅助栈栈顶即最小值
 

复杂度

指标解释
时间O(1)push/pop/top/getMin 都是常数时间
空间O(n)辅助栈最多存 n 个元素

实战考量

频率分析

出现在:字节/阿里/美团 一面设计题,META/TikTok OA 高频。约 25% 常会从这种”结构改造”题开始,看你能不能想到用辅助数据结构维护额外信息

延伸思考

Q:辅助栈能不能优化空间?

A:可以。只在 val <= 当前最小值 时才入辅助栈;pop 时判断被 pop 的值是否等于当前最小值,是则同步 pop。但代码复杂度增加,实践中写同步法更安全。

Q:如果要求 getMax 呢?

A:再加一个 max_stack,完全一样的思路。两个辅助栈分别维护最小值和最大值。

Q:如果要求 getMedian(中位数)呢?

A:两个堆——大根堆存左半部分,小根堆存右半部分(LeetCode 295 题)。这和 getMin/getMax 是不同量级的问题。

Q:如果实现一个队列,能在 O(1) 获取最小值呢?

A:用单调双端队列(deque),维持队首最小。参考滑动窗口最大值那题的思路反过来。

易错点

  • 辅助栈忘记同步出栈 → 最小值信息错乱

  • 空栈时调用 pop/top/getMin 没处理边界

  • 优化版中 val <= min_stack[-1] 的等号不能丢(多个相同最小值的情况)


生活类比

最小栈 → 两张纸记账法

栈是摞起来的报销单。你在旁边另放一摞”到目前为止的最小值”。

每次塞新单子时,比一下旧最小值和新的,哪张小就把哪张记上去。

主席再问最小支出时——一秒都不用等,瞄一眼右边那摞最上面那张就行。

用辅助空间换时间,就是这么朴实无华。


相关题目

题目关系
232用栈实现队列栈的变体应用,另一种设计模式
225用队列实现栈对偶题目

→ 返回题单:LeetCode学习路线图 > 三、栈与队列

速记卡(面试闪卡)

Q1:一句话讲清「155. 最小栈(Min Stack)」到底是什么?

A:设计支持 push/pop/top 且能 O(1) 取最小元素的栈。

Q2:题目 —— 怎么理解?

A:像要一个能随时报最小值的报销摞:实现 MinStack,支持压栈、弹栈、取栈顶,还要在常数时间拿到栈里最小元素。例 push -2,0,-3 后 getMin=-3。

Q3:思路 —— 怎么理解?

A:两张纸记账法:主栈正常存元素,辅助栈(min_stack)同步存”到当前位置的最小值”,每次 push 入栈 min(val, 栈顶)。两栈同高同进同出,辅助栈顶即当前最小值。

Q4:推荐写法 —— 怎么理解?

A:push 时主栈 append(val),辅助栈 append(min(val, 栈顶 or val));pop 两栈同步 pop;top 返回主栈顶;getMin 返回辅助栈顶。结构清晰同步法最不易出错。

Q5:复杂度与实战考量 —— 怎么理解?

A:时间 O(1)(四种操作皆常数),空间 O(n)(辅助栈最多 n 个)。优化版可仅在 val≤当前最小值时入辅助栈省空间;边界要处理空栈;等号不能丢(多个相同最小值)。

Q6:核心速记主线有哪些?

  • 辅助栈同步维护每状态最小值

  • 时间 O(1)、空间 O(n)

  • push 取 min 入辅助栈,pop 同步出

  • 优化可省空间,等号别丢

口诀

A:最小栈用两摞纸,主栈存数辅存最小;

同步进出不分离,栈顶一看便知晓;

push 取小入辅栈,pop 同退莫漏掉;

时间一一空间 n,等号保留防相同。

相关链接