最小栈 图解题解
普通栈查最小值要从头翻,有办法让 getMin 也做到 O(1) 吗?再加一个辅助栈就够了。
像同时维护两本账:主账本记所有数字的进出;副账本只记「每一时刻的最小值」——每压入一个更小(或相等)的数,就在副账本也同步记一条。弹出时,若弹掉的正好是副账本顶端,副账本也跟着弹。于是任何时候翻开副账本顶端,瞬间得到当前最小值,不用重新扫主账本。
这道题到底在问什么
- 操作
- push(-2), push(0), push(-3), getMin, pop, top, getMin
- 输出
- -3, 0, 0
最优解:为什么这么做
一句话答案:LeetCode 155 最小栈的标准解法是辅助栈:主栈正常存数据,辅助栈与主栈同步压弹、每层存「压入这层之后整栈的最小值」,push 时压入 min(新元素, 辅助栈顶),pop 时两栈同弹。getMin 直接读辅助栈顶,push、pop、top、getMin 全部 O(1),代价是 O(n) 的额外空间。
这道题真正在问什么
设计一个栈,除了常规的 push、pop、top,还要提供 getMin——在常数时间内返回栈中当前的最小元素。难点全在「常数时间」四个字:栈里的内容随压弹不断变化,最小值也跟着变,getMin 却不许现场遍历去找。这是一道考「用空间换时间、为查询预留答案」的设计题。
为什么一个最小值变量不够用
最直觉的偷懒办法是维护一个变量 min,push 时顺手更新。压栈没问题,坏在 pop:一旦弹出的恰好是当前最小值,min 该回退成「剩下元素里的最小值」——可这个值是多少,单个变量没有记录,只能重新遍历整个栈去找,O(1) 就破功了。
这暴露了问题的真正需求:我们需要的不是「一个最小值」,而是「每个历史时刻的最小值」,弹栈时才能随时回退到上一个状态。历史状态要跟着栈的压弹同步存取——后压的状态先失效——这本身又是一个后进先出的过程,所以答案呼之欲出:再用一个栈来存最小值的历史。
辅助栈的不变量怎么定义
开一个辅助栈 mn,与主栈 st 保持两条不变量:一是高度永远相等,二是 mn 的第 i 层存「主栈从栈底到第 i 层这一段的最小值」。于是 mn 的栈顶恒等于当前整个主栈的最小值,getMin 读一下栈顶就完事。
维护也只要两条同步规则。push(x) 时:主栈压 x,辅助栈压 min(x, mn 栈顶)——新最小值要么是 x 自己,要么延续原来的最小值;辅助栈为空时直接压 x。pop 时:两栈同时弹一个。注意辅助栈压的是取 min 之后的结果而不是 x 本身,压 x 本身的话栈顶就不再代表全栈最小,整个设计就塌了。
为什么 pop 之后最小值自动恢复正确
关键在于辅助栈每一层的含义是「截至这一层的前缀最小值」,它只由主栈更低的那些层决定,与后来压入的元素无关。弹掉栈顶后,露出来的新栈顶记录的正是「剩余那些元素的最小值」——这个答案在当初压栈时就算好存在那里了,弹栈只是把它重新露出来,不需要任何补算。
这就是本题的核心思想:与其在查询时现找答案,不如在每次入栈时把「此刻的答案」一并存档,出栈时档案随之销毁,任何时刻栈顶档案都与栈内容严格对应。
复杂度怎么算,有哪些进阶变体
四个操作 push、pop、top、getMin 都只做常数次栈操作,时间各是 O(1);辅助栈与主栈一样高,额外空间 O(n)。
空间上有两个常见优化方向:一是辅助栈只在新元素小于等于栈顶时才压入,弹栈时判断相等才同步弹,能省掉大量重复的最小值,但相等情形的处理容易写错;二是「差值法」,主栈存 x 与当前最小值的差、外加一个 min 变量,能把额外空间压到 O(1),代价是要处理编码解码和数值溢出。面试先把基础辅助栈讲清楚,再提这两个变体作为亮点即可。
▶ 动画逐步走查(共 36 步)——想跟着动画一帧帧对照就展开
- 3核心思路:辅助栈 mn 与主栈 st 高度永远相等。mn 的栈顶,始终是「当前主栈里所有元素的最小值」。
- 4push(x):主栈压 x,辅助栈压 min(x, 辅助栈原栈顶)。pop:两栈同时弹一个。这样辅助栈顶恒为最小值。
- 5先看初始状态:主栈和辅助栈都为空。接下来每次 push 两栈同步压入,每次 pop 两栈同步弹出,高度始终相等。
- 6push(-2):主栈压入 -2;辅助栈压入 -2(栈空,直接放) = -2。两栈同时长高一格,辅助栈顶 -2 就是现在主栈的最小值。
- 7push(0):主栈压入 0;辅助栈压入 min(0, 辅助栈顶-2) = -2。两栈同时长高一格,辅助栈顶 -2 就是现在主栈的最小值。
- 8push(-3):主栈压入 -3;辅助栈压入 min(-3, 辅助栈顶-2) = -3。两栈同时长高一格,辅助栈顶 -3 就是现在主栈的最小值。
- 9getMin():直接读辅助栈顶 -3,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 10pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 -3)。两栈高度依旧相等,弹完后辅助栈顶 = -2,自动回退到上一个最小值。
- 11top():返回主栈顶 0。辅助栈只管最小值,不影响 top。
- 12getMin():直接读辅助栈顶 -2,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 13push(5):主栈压入 5;辅助栈压入 min(5, 辅助栈顶-2) = -2。两栈同时长高一格,辅助栈顶 -2 就是现在主栈的最小值。
- 14push(-1):主栈压入 -1;辅助栈压入 min(-1, 辅助栈顶-2) = -2。两栈同时长高一格,辅助栈顶 -2 就是现在主栈的最小值。
- 15push(-4):主栈压入 -4;辅助栈压入 min(-4, 辅助栈顶-2) = -4。两栈同时长高一格,辅助栈顶 -4 就是现在主栈的最小值。
- 16getMin():直接读辅助栈顶 -4,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 17pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 -4)。两栈高度依旧相等,弹完后辅助栈顶 = -2,自动回退到上一个最小值。
- 18top():返回主栈顶 -1。辅助栈只管最小值,不影响 top。
- 19push(-4):主栈压入 -4;辅助栈压入 min(-4, 辅助栈顶-2) = -4。两栈同时长高一格,辅助栈顶 -4 就是现在主栈的最小值。
- 20getMin():直接读辅助栈顶 -4,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 21pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 -4)。两栈高度依旧相等,弹完后辅助栈顶 = -2,自动回退到上一个最小值。
- 22getMin():直接读辅助栈顶 -2,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 23pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 -1)。两栈高度依旧相等,弹完后辅助栈顶 = -2,自动回退到上一个最小值。
- 24pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 5)。两栈高度依旧相等,弹完后辅助栈顶 = -2,自动回退到上一个最小值。
- 25pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 0)。两栈高度依旧相等,弹完后辅助栈顶 = -2,自动回退到上一个最小值。
- 26getMin():直接读辅助栈顶 -2,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 27pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 -2)。两栈高度依旧相等,弹完后辅助栈顶 = (空),自动回退到上一个最小值。
- 28push(6):主栈压入 6;辅助栈压入 6(栈空,直接放) = 6。两栈同时长高一格,辅助栈顶 6 就是现在主栈的最小值。
- 29push(3):主栈压入 3;辅助栈压入 min(3, 辅助栈顶6) = 3。两栈同时长高一格,辅助栈顶 3 就是现在主栈的最小值。
- 30push(9):主栈压入 9;辅助栈压入 min(9, 辅助栈顶3) = 3。两栈同时长高一格,辅助栈顶 3 就是现在主栈的最小值。
- 31push(1):主栈压入 1;辅助栈压入 min(1, 辅助栈顶3) = 1。两栈同时长高一格,辅助栈顶 1 就是现在主栈的最小值。
- 32getMin():直接读辅助栈顶 1,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 33pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 1)。两栈高度依旧相等,弹完后辅助栈顶 = 3,自动回退到上一个最小值。
- 34pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 9)。两栈高度依旧相等,弹完后辅助栈顶 = 3,自动回退到上一个最小值。
- 35pop():主栈和辅助栈同时弹掉栈顶(弹出主栈的 3)。两栈高度依旧相等,弹完后辅助栈顶 = 6,自动回退到上一个最小值。
- 36top():返回主栈顶 6。辅助栈只管最小值,不影响 top。
- 37getMin():直接读辅助栈顶 6,O(1) 拿到当前主栈最小值,完全不用遍历主栈。
- 38最后再 push(7) 收个尾:主栈与辅助栈又同步长高一格,辅助栈顶 7 即此刻最小值。规律始终是「两栈同步、辅助栈顶即最小」。
⚠️ 容易写错的地方
✗ 错:getMin 时现扫一遍主栈找最小
✓ 对:辅助栈顶直接给出,O(1)
每次扫描 O(n),频繁 getMin 会超时
✗ 错:只用一个变量记最小值
✓ 对:必须用栈,因为 pop 后最小值要回退
单变量无法恢复「弹出后」的上一个最小值
✗ 错:push 时辅助栈压入 x 本身
✓ 对:压入 min(x, 辅助栈顶)
辅助栈顶必须始终是当前全栈最小值
完整代码(Python / C++ / Java)
Python
class MinStack:
def __init__(self):
self.st = [] # 主栈
self.mn = [] # 辅助栈:同步存最小值
def push(self, x: int) -> None:
self.st.append(x)
m = x if not self.mn else min(x, self.mn[-1])
self.mn.append(m) # 压入 min(x, 辅助栈顶)
def pop(self) -> None:
self.st.pop()
self.mn.pop() # 两栈同步弹
def top(self) -> int:
return self.st[-1]
def getMin(self) -> int:
return self.mn[-1] # O(1):辅助栈顶即最小值C++
class MinStack {
stack<int> st, mn; // 主栈 + 辅助栈
public:
void push(int x) {
st.push(x);
mn.push(mn.empty() ? x : min(x, mn.top()));
}
void pop() { st.pop(); mn.pop(); } // 同步弹
int top() { return st.top(); }
int getMin() { return mn.top(); } // O(1)
};Java
class MinStack {
private Deque<Integer> st = new ArrayDeque<>(); // 主栈
private Deque<Integer> mn = new ArrayDeque<>(); // 辅助栈
public void push(int x) {
st.push(x);
mn.push(mn.isEmpty() ? x : Math.min(x, mn.peek()));
}
public void pop() { st.pop(); mn.pop(); } // 同步弹
public int top() { return st.peek(); }
public int getMin() { return mn.peek(); } // O(1)
}复杂度
时间
O(1) / 操作
push/pop/top/getMin 都是常数次栈操作
空间
O(n)
辅助栈和主栈一样高,额外 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 最小栈 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不用辅助栈、只用 O(1) 额外空间?+
可以用「差值法」:主栈存 (x - 当前min),并单独维护一个 min 变量。压入差值、解码时还原,能省掉辅助栈,但实现更绕、易出错。面试讲清辅助栈法即可,差值法作为进阶亮点。
辅助栈里存重复的最小值会不会浪费?+
可以优化成「辅助栈存 (最小值, 出现次数)」或只在 x ≤ 栈顶时才压入;但要小心 pop 时的计数维护。基础版每步都压同步性最好、最不易错。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 最小栈 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。