LeetCode经典栈类题目——155. 最小栈,这道题的核心考点是「如何在常数时间内获取栈的最小元素」,看似简单,但如果直接暴力求解会超时,所以需要一个巧妙的辅助思路。话不多说,我们一步步来拆解。
一、题目解读:到底要我们做什么?
题目要求设计一个MinStack类,支持栈的基础操作(push、pop、top),同时额外要求「能在常数时间O(1)内检索到最小元素」。这里有两个关键注意点,也是容易踩坑的地方:
-
基础操作要符合栈的特性:后进先出(LIFO),push是压入栈顶,pop是删除栈顶,top是获取栈顶元素(不删除);
-
核心难点:getMin()方法必须是O(1)时间复杂度。如果每次getMin()都遍历整个栈找最小值,时间复杂度是O(n),会超时,所以必须提前缓存最小值。
补充题目给出的类方法要求,避免遗漏:
-
MinStack():初始化两个空栈(后面会说为什么是两个);
-
push(int val):将val压入主栈,同时处理辅助栈(缓存最小值);
-
pop():删除主栈顶元素,若删除的是当前最小值,同步删除辅助栈顶;
-
top():返回主栈顶元素,若栈空返回null;
-
getMin():返回辅助栈顶元素(即当前最小值),若栈空返回null。
二、解题思路:双栈协同,缓存最小值
这道题的最优解法是「双栈法」,两个栈各司其职,协同工作,既保证基础操作的效率,又实现O(1)取最小。
1. 双栈分工(核心逻辑)
-
主栈(stack):负责存储所有元素,执行正常的push、pop、top操作,和普通栈完全一致;
-
辅助栈(minStack):专门缓存「当前栈中的最小值」,栈顶永远是主栈中当前的最小元素。
为什么这样设计?因为辅助栈可以实时记录主栈中每一步的最小值,当主栈操作时,辅助栈同步更新,这样getMin()只需要取辅助栈顶即可,完全不用遍历。
2. 关键细节(避坑重点)
辅助栈的更新规则是这道题的灵魂,也是最容易写错的地方,一定要注意两个点:
-
push时:只有当「辅助栈为空」,或者「当前要压入的val ≤ 辅助栈顶元素」时,才将val压入辅助栈。这里用「≤」而不是「<」,是为了处理「主栈中有多个相同最小值」的情况(比如连续push 2、1、1,辅助栈也要push 2、1、1,否则pop第一个1后,辅助栈顶变成2,就错了);
-
pop时:只有当「主栈删除的元素(栈顶元素)等于辅助栈顶元素」时,才删除辅助栈顶元素。因为此时删除的是当前最小值,辅助栈需要同步更新,否则辅助栈顶会保留已经被删除的最小值,导致getMin()出错。
3. 逻辑演示(直观理解)
我们用一组操作演示双栈的变化,帮大家快速吃透逻辑:
-
初始化:stack = [], minStack = [];
-
push(3):stack = [3],minStack为空,push 3 → minStack = [3];
-
push(2):stack = [3,2],2 ≤ 3(minStack顶),push 2 → minStack = [3,2];
-
push(2):stack = [3,2,2],2 ≤ 2(minStack顶),push 2 → minStack = [3,2,2];
-
getMin():返回minStack顶 → 2(正确);
-
pop():stack删除2 → [3,2],删除的2等于minStack顶,minStack删除2 → [3,2];
-
getMin():返回minStack顶 → 2(正确,此时主栈最小还是2);
-
pop():stack删除2 → [3],删除的2等于minStack顶,minStack删除2 → [3];
-
getMin():返回minStack顶 → 3(正确);
-
top():返回stack顶 → 3(正确)。
三、完整代码解析(TypeScript版)
题目已经给出了核心代码,下面逐行解析每个方法的逻辑,重点标注易错点和核心细节,确保大家能看懂每一步的意义。
class MinStack {
// 定义两个栈:主栈存储元素,辅助栈缓存最小值
stack: number[];
minStack: number[];
// 构造函数:初始化两个空栈
constructor() {
this.stack = [];
this.minStack = [];
}
// push方法:压入元素,同步更新辅助栈
push(val: number): void {
// 1. 先将元素压入主栈(基础操作)
this.stack.push(val);
// 2. 辅助栈为空 或 当前val ≤ 辅助栈顶 → 压入辅助栈(缓存最小值)
if(this.minStack.length === 0 || val<= this.minStack[this.minStack.length - 1]){
this.minStack.push(val);
}
}
// pop方法:删除栈顶元素,同步维护辅助栈
pop(): void {
// 边界处理:如果主栈/辅助栈为空,直接返回(避免报错)
if (this.stack.length === 0) {
return;
}
if(this.minStack.length === 0){
return;
}
// 1. 删除主栈顶元素,并保存删除的值
const popped = this.stack.pop();
// 2. 若删除的值等于辅助栈顶(即当前最小值),同步删除辅助栈顶
if (popped === this.minStack[this.minStack.length - 1]) {
this.minStack.pop();
}
}
// top方法:获取主栈顶元素(不删除)
top(): number | null {
// 边界处理:栈空返回null
if (this.stack.length === 0) {
return null;
}
// 返回主栈顶元素(数组最后一个元素)
return this.stack[this.stack.length - 1];
}
// getMin方法:获取当前最小值(O(1)时间)
getMin(): number | null {
// 边界处理:辅助栈空(主栈也空)返回null
if (this.minStack.length === 0) {
return null;
}
// 辅助栈顶就是当前最小值
return this.minStack[this.minStack.length - 1];
}
}
/**
* 实例化使用示例(帮助理解如何调用)
* var obj = new MinStack()
* obj.push(val) // 压入元素
* obj.pop() // 删除栈顶
* var param_3 = obj.top() // 获取栈顶
* var param_4 = obj.getMin() // 获取最小值
*/
代码易错点总结
-
push方法中,辅助栈的判断条件必须是「≤」,不能是「<」,否则多重复最小值会出错;
-
pop方法中,必须先保存主栈删除的元素(popped),再和辅助栈顶比较,不能直接用this.stack.pop()和辅助栈顶比较(因为pop()会直接修改主栈);
-
所有方法都要做「栈空」的边界处理,否则当栈为空时,调用pop()、top()、getMin()会报错(比如访问undefined的length)。
四、复杂度分析(面试必问)
这道题的双栈解法,时间和空间复杂度都很优,面试时被问到复杂度分析,直接按下面说即可:
-
时间复杂度:push、pop、top、getMin四个方法,每一步操作都是O(1)。因为每个元素最多被压入和弹出栈各一次,没有遍历操作;
-
空间复杂度:O(n)。最坏情况下(比如所有元素按从大到小顺序压入),辅助栈会存储和主栈一样多的元素,n是压入栈的元素个数。
补充:有没有空间更优的解法?有(比如用一个栈,同时存储元素和当前最小值的差值),但会增加代码复杂度,且容易出错。双栈解法是「空间换时间」的经典案例,代码简洁、逻辑清晰,面试时优先写这种解法即可。
五、面试高频追问(提前准备)
这道题是栈类题目中的高频题,面试时很容易被追问,整理了2个最常问的问题,帮大家提前避坑:
-
追问1:为什么辅助栈要存重复的最小值? 答:因为当主栈中有多个相同的最小值时,删除其中一个,剩下的最小值依然存在。如果辅助栈只存一次,删除后辅助栈顶就会变成上一个更小值(不存在),导致getMin()出错。
-
追问2:如果pop的时候,主栈和辅助栈都不为空,但弹出的元素不等于辅助栈顶,为什么不处理辅助栈? 答:因为辅助栈顶是当前主栈的最小值,弹出的元素不是最小值,所以最小值依然存在于辅助栈顶,不需要处理,getMin()依然能拿到正确结果。
六、总结
LeetCode 155. 最小栈的核心是「双栈协同」,用辅助栈缓存最小值,实现O(1)时间获取最小元素。解题的关键在于掌握辅助栈的更新规则(push时≤才压入,pop时相等才弹出),同时做好边界处理。
这道题难度不算高,但非常经典,是理解「栈的特性」和「空间换时间」思想的绝佳题目。代码量不大,但每一步都有讲究,尤其是辅助栈的细节,一定要多动手演示几遍,避免踩坑。